forked from SpaceStation14-Shenanigans/Monolith
Compare commits
545 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8c844578 | |||
| a9e4468d78 | |||
| f3919fafc1 | |||
| 974b78863b | |||
| 1654bd5cf8 | |||
| d353667f97 | |||
| 47116d8ba7 | |||
| 9bd6d5f3f2 | |||
| 1dc696deb6 | |||
| f2df23dc81 | |||
| 76d6ea96e0 | |||
| 93ba265e00 | |||
| 16f73ec031 | |||
| 9d3b0778b3 | |||
| 32b15951e2 | |||
| 7a0f0d04c5 | |||
| bcce8e67a3 | |||
| 5afcdd9379 | |||
| 01c7ea464a | |||
| 55f6108312 | |||
| 8be27c1658 | |||
| 4df38cc9cc | |||
| 8c0d2a50df | |||
| 2d8105de13 | |||
| 01bb73bf56 | |||
| fb09b866f2 | |||
| 0458a500ff | |||
| 4334b07093 | |||
| ad2dd99a9b | |||
| f11c83a96e | |||
| 36d1740829 | |||
| 1e201bb8d2 | |||
| c7a34dfc22 | |||
| e366c4d7cc | |||
| bc0151e540 | |||
| b2307660de | |||
| 5d5f6b4654 | |||
| 165f9950b5 | |||
| ef9858936d | |||
| d0a95f5d8b | |||
| ee386b0633 | |||
| de38cc18ef | |||
| ca928bdc83 | |||
| cee6ce9566 | |||
| 984a2c9b71 | |||
| 548e9eccb6 | |||
| cd817abd2c | |||
| f276df8fe9 | |||
| dd323be01d | |||
| 0cf9b35f08 | |||
| 114a09f290 | |||
| dc4b50e71e | |||
| c405c32c89 | |||
| 34ff30a736 | |||
| c8e162baa1 | |||
| ffcbc32c43 | |||
| 351b225102 | |||
| 496432881e | |||
| 9fd2a7b5af | |||
| 8972408ec1 | |||
| 5798b57788 | |||
| ec88162ac0 | |||
| 3992aa846f | |||
| 4b62f35f10 | |||
| cc1c91f39f | |||
| c5241854ce | |||
| d02895a50f | |||
| c41d005cf4 | |||
| 013f57a73d | |||
| c3f687a493 | |||
| f837710be1 | |||
| 7b00d623e0 | |||
| dafbb5f3d8 | |||
| d4487d9a5a | |||
| 7b9f602c55 | |||
| 97509dd3b6 | |||
| 55c2a03ea4 | |||
| 543c19645f | |||
| da8b19dd05 | |||
| 4fc44d3a9c | |||
| 55688cc9e0 | |||
| 5357469644 | |||
| 6040135534 | |||
| 52649a871e | |||
| fef33511d8 | |||
| fec5bfce0a | |||
| 11f82f95d3 | |||
| 3dbad97455 | |||
| a193ab828e | |||
| c75083140d | |||
| 106f9b6d28 | |||
| fe24fc22bc | |||
| 85c9636003 | |||
| a1d2f4dcbc | |||
| 3381fe165c | |||
| a913c35b21 | |||
| 0706b759d4 | |||
| 73a91c0b0a | |||
| 9e390680bd | |||
| d3d391f5a0 | |||
| a6506c7f46 | |||
| 958421b6c9 | |||
| c513938ed9 | |||
| a6431c9bab | |||
| f3390a81cc | |||
| 6ba80381cc | |||
| ef095aec88 | |||
| d02738089c | |||
| b9f0de8576 | |||
| 5a6d4b74b1 | |||
| 1ac13bf597 | |||
| 90ee912816 | |||
| 27665829e6 | |||
| e1459b0d43 | |||
| 395d334dd7 | |||
| 75e166e9b3 | |||
| d987cfe88d | |||
| a942f3e7b1 | |||
| 4108933834 | |||
| 6af6f3b386 | |||
| 9a590069d2 | |||
| a60321f201 | |||
| dfabb71045 | |||
| ce034c3f19 | |||
| 79c9dee112 | |||
| 6a048def33 | |||
| e41ef48441 | |||
| 27e7b2f70a | |||
| 2bd317dadb | |||
| 1145822797 | |||
| 27dca32ef3 | |||
| 4bdad8f6ee | |||
| 38ebc2699c | |||
| 82f75ff37f | |||
| c84cde54cb | |||
| f0c6624762 | |||
| 277ad91140 | |||
| b9419b25a1 | |||
| e1a5488f14 | |||
| d9b46ca6dc | |||
| a672e9bd28 | |||
| 2a5fe8fe6a | |||
| e069f2ba9d | |||
| db3675b3e8 | |||
| 79bf0fec68 | |||
| 41b3fb9113 | |||
| 7cd749dfa6 | |||
| eb44deeeda | |||
| 7035e752a9 | |||
| 2fdd1cd573 | |||
| 6c4101e0c0 | |||
| 5f90433697 | |||
| 9f54374593 | |||
| 5272291275 | |||
| 4a0f4cec3a | |||
| 5d68716090 | |||
| a4b1a5317c | |||
| ee48853724 | |||
| a8651f6457 | |||
| 47378ac726 | |||
| 9ca8d4a82a | |||
| f8e940c5f5 | |||
| 89b73cdc76 | |||
| 881ee08f91 | |||
| 19d869bda1 | |||
| 964b5768fd | |||
| 925933bbcb | |||
| 277086116a | |||
| 54412b25e4 | |||
| b75be35a1d | |||
| 1d184cfd27 | |||
| ea8ce6d5b5 | |||
| a9a05d0da3 | |||
| e2c193f7c7 | |||
| 060a7565c5 | |||
| 2d94504508 | |||
| 9ec05018b0 | |||
| 9574d6fe1f | |||
| 937131c2a4 | |||
| f2cd5a6f3a | |||
| 7749344876 | |||
| cda0c1f802 | |||
| ad37c2895a | |||
| 7ceedb6014 | |||
| a8c0d92351 | |||
| 1ead85c1c5 | |||
| 135335f7c9 | |||
| c1a5f4d8c5 | |||
| 9f204d62d1 | |||
| 7ef79b3fda | |||
| 3f0670b985 | |||
| c7060a0dc3 | |||
| 6a48576e4e | |||
| e4412cfbb6 | |||
| a119933e1a | |||
| 7602c54c29 | |||
| f58931c915 | |||
| 686d6c8c37 | |||
| e8850079f0 | |||
| 9cfdbd75e0 | |||
| 100800d09c | |||
| 863cd54169 | |||
| b4bd44a589 | |||
| 8ab20f7058 | |||
| 29e20df5b0 | |||
| e7b1f19944 | |||
| 615cc446e0 | |||
| 4e794d6a42 | |||
| 63ac33ebce | |||
| d5d89acbb4 | |||
| 1cee621041 | |||
| 9dbf4629a0 | |||
| 67e804698e | |||
| b63dd431e8 | |||
| 849b094574 | |||
| 2dcf189143 | |||
| 9e4a84a3b2 | |||
| e69a387839 | |||
| 3f50e9e035 | |||
| 6b1977d0f9 | |||
| b9c02cd366 | |||
| f35f5e264d | |||
| 60c6741960 | |||
| 94812b996b | |||
| b291b1cfd6 | |||
| 96902972d5 | |||
| 45bce4ed33 | |||
| 9347f67507 | |||
| e3635d4c91 | |||
| a40b954978 | |||
| adb4296d38 | |||
| a48a235bf6 | |||
| 176055630f | |||
| ff6bf1c131 | |||
| 1be801abf4 | |||
| 0f25271add | |||
| 24bc95450a | |||
| 77ebbdad20 | |||
| 44a6d7eca6 | |||
| eb5c8ac4e9 | |||
| 188070b757 | |||
| 3f97487c0b | |||
| c7de9b660a | |||
| 4b44b891d3 | |||
| b895b2ba42 | |||
| 8a6b39a560 | |||
| ad7633023b | |||
| 8777215b34 | |||
| 4007b079f8 | |||
| 79067884e1 | |||
| ae72c4b217 | |||
| 12485a5ce2 | |||
| 53b74938fd | |||
| 42c12489e3 | |||
| 3c31450ab3 | |||
| 273ca2cd2c | |||
| 64a800ab06 | |||
| 1fec90e647 | |||
| 389b1a39fe | |||
| 8ef3e658b4 | |||
| b6623e107b | |||
| 136225a067 | |||
| 7e88f38fa9 | |||
| 5abbb4beb3 | |||
| cdcb4454b0 | |||
| 4185d83e64 | |||
| 8f921ed705 | |||
| 9a454d9b9a | |||
| 6d5e1cb119 | |||
| 44b443bdf9 | |||
| 696736da06 | |||
| 926c4fab49 | |||
| a80e0db8a0 | |||
| c4954e4bd2 | |||
| 981017a9b4 | |||
| 84b90912f9 | |||
| 78d90096a2 | |||
| a9a5f194f5 | |||
| bc85b73fee | |||
| 0f316130e4 | |||
| 275ecb8e01 | |||
| a3ee39de70 | |||
| 9d38f200d7 | |||
| 8350c4d544 | |||
| e4d67e8d33 | |||
| 278d73dd78 | |||
| 3ab8f43ea3 | |||
| 0bd1b0800e | |||
| c0a10d659a | |||
| bf89ec2037 | |||
| 77811a2d7f | |||
| fa7b973bb3 | |||
| b686b65058 | |||
| 361c0111ca | |||
| c62943f2e0 | |||
| 701883e108 | |||
| b68630257f | |||
| 7072751108 | |||
| 469c42f2a4 | |||
| dd8070316f | |||
| 6e0dbdd1e7 | |||
| ef47aad488 | |||
| 7f00a4c13a | |||
| a651e15ab5 | |||
| f089bf5a29 | |||
| 785e6abe69 | |||
| 6c0061a2d3 | |||
| c3e008866a | |||
| 8245850cb0 | |||
| 5da958f7a9 | |||
| 27513e7a28 | |||
| c39c6ca2f5 | |||
| 319b12b138 | |||
| 21ba67d599 | |||
| 454f33ad43 | |||
| ad9093094c | |||
| 09c7bf422b | |||
| d24a85c6d8 | |||
| 8709244f6f | |||
| 856093613e | |||
| 6b1aabb8f5 | |||
| 2939e6b9bc | |||
| 9476e003c2 | |||
| 6e220606fb | |||
| a56db3da27 | |||
| f7960e90ab | |||
| 313e4f1db6 | |||
| d9721a8328 | |||
| 8100503e6a | |||
| a0f3d8193b | |||
| cacbaec4cf | |||
| d5e213abd9 | |||
| eacac0b2e3 | |||
| b3e51ba5a9 | |||
| d54cc99491 | |||
| 51797e0933 | |||
| 9d97920e55 | |||
| 68da6b039c | |||
| 9a8c9b76e2 | |||
| 5d0fe0a94b | |||
| 88c1e7fd1d | |||
| 3f594f2387 | |||
| b19ec1eb9f | |||
| f76e2bb807 | |||
| ee02f8afdd | |||
| 0a4753edae | |||
| d0ceadf130 | |||
| 05c046d055 | |||
| b1aa743a40 | |||
| 5125fc084e | |||
| 2fa487e4b3 | |||
| d63c42b0ee | |||
| da16de09a5 | |||
| 58fa85d648 | |||
| 3ce7df2433 | |||
| a11952bdd2 | |||
| 22c29f0a46 | |||
| 39358d68cd | |||
| 52cd9ad5c5 | |||
| 3b272d0572 | |||
| fa47f767a3 | |||
| 94d15b0fcf | |||
| 9b20d3aee4 | |||
| 13a4c7eb63 | |||
| a364ff074b | |||
| 6c33c74260 | |||
| e03bc781c8 | |||
| a76089a1d8 | |||
| 14c3ca4ed6 | |||
| 6378076a5d | |||
| e01cb2be78 | |||
| a2cbcddd12 | |||
| 70b903467e | |||
| fdb9954a22 | |||
| 01e622b43d | |||
| 1154e94757 | |||
| d95e82922d | |||
| 24e77e2fc4 | |||
| f15d0e74c9 | |||
| 1107c9c288 | |||
| 57c5287e02 | |||
| 838a56a68a | |||
| 8134e4733e | |||
| b7fc2cc7e7 | |||
| b5618452d5 | |||
| 3c697015a6 | |||
| 5be002dbc2 | |||
| e8989e1fc3 | |||
| a5fcf434ec | |||
| 8ee21957a3 | |||
| 88c6b190b5 | |||
| e490f5216c | |||
| 4da4b3b01a | |||
| f457d5c33c | |||
| f2726120f6 | |||
| 04a3fc2949 | |||
| 3e38958cab | |||
| 0642406657 | |||
| c62bade639 | |||
| 1365399931 | |||
| b02070a3c0 | |||
| 94a7413968 | |||
| c984c461c7 | |||
| e102989c28 | |||
| f8b1355bbb | |||
| 52e11f7a59 | |||
| 5e152a2875 | |||
| 3f40d8f59f | |||
| 56727c248c | |||
| 56dbf295c2 | |||
| 1f5f3d38d5 | |||
| 86f70184d7 | |||
| 3a1f36ea15 | |||
| 036870c2d0 | |||
| 108864d7ed | |||
| 4a21e19afe | |||
| 1d53b774b4 | |||
| 6f3508f497 | |||
| faf89dc740 | |||
| 2e2c0e2a97 | |||
| 424c5001a1 | |||
| e845a6f77c | |||
| e6e9346979 | |||
| 30a6e47d6f | |||
| 298ed43ee5 | |||
| c992c56fa3 | |||
| fe071126d5 | |||
| 86d46357bd | |||
| 32ab30233f | |||
| afb33ea720 | |||
| d83e97be76 | |||
| 77323026ae | |||
| f9d115f708 | |||
| 0fffae63af | |||
| 4848763feb | |||
| b52fea4920 | |||
| ca50c40b3b | |||
| 5d8b66f491 | |||
| 0e717b1cd6 | |||
| 3da48f94d1 | |||
| 92eaee9dc0 | |||
| 9945dcc5fa | |||
| e91ee50973 | |||
| abf7764557 | |||
| b5d0ce7866 | |||
| f9f4c37b03 | |||
| 0d340bbae9 | |||
| 81a1371eb4 | |||
| 2825f0f400 | |||
| c218faac8d | |||
| 1fb4bca1b3 | |||
| 846fb711a6 | |||
| c943010bc1 | |||
| c497df354b | |||
| f692d16b1a | |||
| 885c2feec3 | |||
| ab4e4daa48 | |||
| 8743651e1b | |||
| ccff51b77b | |||
| 52915390bb | |||
| 09150f5a7d | |||
| 9ea3e34f49 | |||
| 1ca09e96d3 | |||
| 44aac3d94b | |||
| 75f18469aa | |||
| 956658b305 | |||
| 297262017b | |||
| 7536925649 | |||
| d463794da7 | |||
| ee903e3735 | |||
| 3f9e35f900 | |||
| 90b2bd34e2 | |||
| 0fe194c209 | |||
| 3cbfc5055c | |||
| c4d5fa6813 | |||
| 41e8348d96 | |||
| f715d4c00b | |||
| 685d12c35f | |||
| 1182332c0d | |||
| 9ce9d7258d | |||
| 23489aaadd | |||
| b36192bf3b | |||
| 417cd92c4b | |||
| 7561cdfb92 | |||
| dfe9c65221 | |||
| 7f852a8cb8 | |||
| 6895525e91 | |||
| dc230618e3 | |||
| d5530fb5ee | |||
| 1a6bd945c5 | |||
| 69c0484989 | |||
| c7f86e6b6f | |||
| 8cb67fbbe2 | |||
| 4c7d77502c | |||
| 7b6ebef36f | |||
| 974cb92dc5 | |||
| 4728ca23d7 | |||
| 7e4824a145 | |||
| 977815809c | |||
| 86aa49c368 | |||
| ba6846858f | |||
| 9b82f44c47 | |||
| 658651d9f5 | |||
| c0e2eb143d | |||
| f868adde71 | |||
| 0168527964 | |||
| f8ada83ac6 | |||
| af9e5f9e39 | |||
| a5aec5f2d2 | |||
| be00b6647f | |||
| 46bfa7a456 | |||
| 7c72df2b93 | |||
| 7f5c9e7b54 | |||
| 977407aac6 | |||
| 5f1b01604c | |||
| 9b763e1337 | |||
| 50c992f678 | |||
| f9d33d734e | |||
| 3d0bb2e741 | |||
| 46be764897 | |||
| 454131d131 | |||
| 7a835183eb | |||
| 569657571c | |||
| bf30edf019 | |||
| e11d29b99f | |||
| 3b4239d733 | |||
| 2679fd6dd4 | |||
| 8780e7cc4b | |||
| 4a955f1f5b | |||
| 3c2486489a | |||
| 1bf0ea7b0c | |||
| 012421b262 | |||
| 23712f4814 | |||
| dba2b120aa | |||
| fcf4847936 | |||
| b6e4b9cb42 | |||
| b025902141 | |||
| 154d8dcfed | |||
| ca6b3e70ad | |||
| 4ddc73ce0b | |||
| a217ba1885 | |||
| 20e456b3ed | |||
| efa7daa13c | |||
| 498a96042f | |||
| 4c8a74d0fd |
+1
-1
@@ -348,7 +348,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
|
||||
resharper_csharp_prefer_qualified_reference = false
|
||||
resharper_csharp_allow_alias = false
|
||||
|
||||
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,json}]
|
||||
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,json,slnx}]
|
||||
indent_size = 2
|
||||
|
||||
[nuget.config]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<!-- Guidelines: https://docs.spacestation14.io/en/getting-started/pr-guideline -->
|
||||
|
||||
## About the PR
|
||||
<!-- What did you change? -->
|
||||
@@ -7,23 +6,23 @@
|
||||
## Why / Balance
|
||||
<!-- Discuss how this would affect game balance or explain why it was changed. Link any relevant discussions or issues. -->
|
||||
|
||||
## How to test
|
||||
<!-- Describe the way it can be tested -->
|
||||
|
||||
## Media
|
||||
<!-- Attach media if the PR makes ingame changes (clothing, items, features, etc).
|
||||
Small fixes/refactors are exempt. Media may be used in SS14 progress reports with credit. -->
|
||||
|
||||
## Requirements
|
||||
<!-- Confirm the following by placing an X in the brackets [X]: -->
|
||||
- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
|
||||
- [ ] I have read relevant guidelines/documentation to this PR found on [our devwiki](https://monolith-station.github.io/mono-docs/).
|
||||
- [ ] I have added media to this PR or it does not require an ingame showcase.
|
||||
- [ ] I can confirm this PR contains no AI-generated content, and did not use any AI-generated content.
|
||||
- [ ] I can confirm this PR contains either no AI-generated content, or AI-generated content that meets our guidelines.
|
||||
<!-- You should understand that not following the above may get your PR closed at maintainer’s discretion -->
|
||||
<!-- When using AI, make sure you are already proficient in creating whatever content you are attempting to generate and that you already have experience contributing to SS14. This means using AI to enhance your preexisting workflow, not vibecoding. -->
|
||||
|
||||
## How to test
|
||||
<!-- Describe the way it can be tested -->
|
||||
|
||||
## Breaking changes
|
||||
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them.
|
||||
This will be posted in #codebase-changes. -->
|
||||
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them. -->
|
||||
|
||||
**Changelog**
|
||||
<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: PR Changelogs
|
||||
concurrency: commit_action
|
||||
concurrency: commit_action-${{ github.event.pull_request.base.ref }} # Mono
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
@@ -16,11 +16,11 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
- name: Checkout Base Branch # Mono
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.BOT_TOKEN }}
|
||||
ref: "${{ vars.CHANGELOG_BRANCH }}"
|
||||
ref: "${{ github.event.pull_request.base.ref }}" # Mono
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
@@ -47,9 +47,9 @@ jobs:
|
||||
|
||||
- name: Commit Changelog
|
||||
run: |
|
||||
git pull origin main
|
||||
git pull origin "${{ github.event.pull_request.base.ref }}"
|
||||
git add "${{ env.CHANGELOG_DIR }}"
|
||||
git commit -m "Automated Changelog (#${{ env.PR_NUMBER }})"
|
||||
git push
|
||||
git push origin "${{ github.event.pull_request.base.ref }}"
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# MSbuild binlog files
|
||||
*.binlog
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
|
||||
Vendored
+53
-7
@@ -13,6 +13,26 @@
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Client - Tools",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build - Tools",
|
||||
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
|
||||
"args": [],
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Client - Release",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build - Release",
|
||||
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
|
||||
"args": [],
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Client (Compatibility renderer)",
|
||||
"type": "coreclr",
|
||||
@@ -31,6 +51,26 @@
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Server - Tools",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build - Tools",
|
||||
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
|
||||
"args": [""],
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Server - Release",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build - Release",
|
||||
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
|
||||
"args": [""],
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "YAML Linter",
|
||||
"type": "coreclr",
|
||||
@@ -40,7 +80,7 @@
|
||||
"cwd": "${workspaceFolder}/Content.YAMLLinter",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
@@ -51,14 +91,21 @@
|
||||
],
|
||||
"preLaunchTask": "build"
|
||||
},
|
||||
// Frontier begin
|
||||
{
|
||||
"name": "Server/Client (Release)",
|
||||
"name": "Server/Client - Tools",
|
||||
"configurations": [
|
||||
"Server",
|
||||
"Client"
|
||||
"Server - Tools",
|
||||
"Client - Tools"
|
||||
],
|
||||
"preLaunchTask": "build-release"
|
||||
"preLaunchTask": "build - Tools"
|
||||
},
|
||||
{
|
||||
"name": "Server/Client - Release",
|
||||
"configurations": [
|
||||
"Server - Release",
|
||||
"Client - Release"
|
||||
],
|
||||
"preLaunchTask": "build - Release"
|
||||
},
|
||||
{
|
||||
"name": "Server/Client (No Build)",
|
||||
@@ -67,6 +114,5 @@
|
||||
"Client"
|
||||
]
|
||||
}
|
||||
// Frontier end
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+26
-6
@@ -21,26 +21,46 @@
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
// Frontier begin
|
||||
{
|
||||
"label": "build-release",
|
||||
"label": "build - Tools",
|
||||
"command": "dotnet",
|
||||
"type": "shell",
|
||||
"args": [
|
||||
"build",
|
||||
"--configuration=Release", // Build in release mode. Note: --, not /. /configuration doesn't work, because Microsoft.
|
||||
"--configuration",
|
||||
"Tools",
|
||||
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
|
||||
"/consoleloggerparameters:NoSummary" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
|
||||
"/consoleloggerparameters:'ForceNoAlign;NoSummary'", // Do not generate summary otherwise it leads to duplicate errors in Problems panel
|
||||
],
|
||||
"group": {
|
||||
"kind": "build"
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build - Release",
|
||||
"command": "dotnet",
|
||||
"type": "shell",
|
||||
"args": [
|
||||
"build",
|
||||
"--configuration",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
|
||||
"/consoleloggerparameters:'ForceNoAlign;NoSummary'", // Do not generate summary otherwise it leads to duplicate errors in Problems panel
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
// Frontier end
|
||||
{
|
||||
"label": "build-yaml-linter",
|
||||
"command": "dotnet",
|
||||
|
||||
@@ -8,7 +8,7 @@ import shutil
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
|
||||
SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
|
||||
# If this doesn't match the saved version we overwrite them all.
|
||||
CURRENT_HOOKS_VERSION = "2"
|
||||
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<OutputPath>..\bin\Content.Benchmarks\</OutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
|
||||
<!-- pin transitive deps -->
|
||||
<PackageReference Include="System.Management" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
|
||||
@@ -19,10 +22,12 @@
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
|
||||
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Server.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Benchmarks.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Testing.props" />
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Global usings for Content.Benchmarks
|
||||
|
||||
global using Robust.UnitTesting.Pool;
|
||||
@@ -0,0 +1,253 @@
|
||||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Atmos.Overlays;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a thermal heatmap overlay for gas tiles, used for equipment like thermal glasses.
|
||||
/// /// </summary>
|
||||
public sealed class GasTileDangerousTemperatureOverlay : Overlay
|
||||
{
|
||||
public override bool RequestScreenTexture { get; set; } = false;
|
||||
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
private GasTileOverlaySystem? _gasTileOverlay;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
private EntityQuery<GasTileOverlayComponent> _overlayQuery;
|
||||
|
||||
private IRenderTexture? _temperatureTarget;
|
||||
|
||||
// Cache used to transform ThermalByte into Color for overlay
|
||||
private readonly Color[] _colorCache = new Color[256];
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
public GasTileDangerousTemperatureOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||
|
||||
_overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
|
||||
for (byte i = 0; i <= ThermalByte.TempResolution; i++)
|
||||
{
|
||||
_colorCache[i] = PreCalculateColor(i);
|
||||
}
|
||||
|
||||
_colorCache[ThermalByte.StateVacuum] = Color.Teal;
|
||||
_colorCache[ThermalByte.StateVacuum].A = 0.6f;
|
||||
_colorCache[ThermalByte.AtmosImpossible] = Color.Transparent;
|
||||
|
||||
#if DEBUG // This shouldn't happend so tell me if you see this LimeGreen on the screen
|
||||
_colorCache[ThermalByte.ReservedFuture0] = Color.LimeGreen;
|
||||
_colorCache[ThermalByte.ReservedFuture1] = Color.LimeGreen;
|
||||
_colorCache[ThermalByte.ReservedFuture2] = Color.LimeGreen;
|
||||
#else
|
||||
_colorCache[ThermalByte.ReservedFuture0] = Color.Transparent;
|
||||
_colorCache[ThermalByte.ReservedFuture1] = Color.Transparent;
|
||||
_colorCache[ThermalByte.ReservedFuture2] = Color.Transparent;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used for Calculating onscreen color from ThermalByte core value
|
||||
/// /// </summary>
|
||||
private static Color PreCalculateColor(byte byteTemp)
|
||||
{
|
||||
// Color Thresholds in Kelvin
|
||||
// -150 C
|
||||
const float deepFreezeK = 123.15f;
|
||||
// -50 C
|
||||
const float freezeStartK = 223.15f;
|
||||
// 0 C
|
||||
const float waterFreezeK = 273.15f;
|
||||
// 50 C
|
||||
const float heatStartK = 323.15f;
|
||||
// 100 C
|
||||
const float waterBoilK = 373.15f;
|
||||
// 300 C
|
||||
const float superHeatK = 573.15f;
|
||||
|
||||
var tempK = byteTemp * ThermalByte.TempDegreeResolution;
|
||||
|
||||
// Neutral Zone Check (0C to 50C)
|
||||
// If between 273.15K and 323.15K, it's transparent.
|
||||
if (tempK >= waterFreezeK && tempK < heatStartK)
|
||||
{
|
||||
return Color.Transparent;
|
||||
}
|
||||
|
||||
Color resultingColor;
|
||||
|
||||
switch (tempK)
|
||||
{
|
||||
case < deepFreezeK:
|
||||
resultingColor = Color.FromHex("#330066");
|
||||
resultingColor.A = 0.7f;
|
||||
break;
|
||||
case < freezeStartK:
|
||||
// Interpolate Deep Purple -> Blue
|
||||
// Range: 123.15 to 223.15 (Span: 100)
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
Color.FromHex("#330066"),
|
||||
Color.Blue,
|
||||
(tempK - deepFreezeK) * 0.01f);
|
||||
resultingColor.A = 0.6f;
|
||||
break;
|
||||
case < waterFreezeK:
|
||||
// Interpolate Blue -> Transparent
|
||||
// Range: 223.15 to 273.15 (Span: 50)
|
||||
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.6f),
|
||||
new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.2f),
|
||||
(tempK - freezeStartK) * 0.02f);
|
||||
break;
|
||||
case < waterBoilK:
|
||||
// Interpolate Transparent -> Yellow
|
||||
// Range: 323.15 to 373.15 (Span: 50)
|
||||
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.2f),
|
||||
new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.6f),
|
||||
(tempK - heatStartK) * 0.02f);
|
||||
break;
|
||||
case < superHeatK:
|
||||
// Interpolate Yellow -> Red
|
||||
// Range: 373.15 to 573.15 (Span: 200)
|
||||
resultingColor = Color.InterpolateBetween(
|
||||
Color.Yellow,
|
||||
Color.Red,
|
||||
(tempK - waterBoilK) * 0.005f);
|
||||
resultingColor.A = 0.6f;
|
||||
break;
|
||||
default:
|
||||
resultingColor = Color.DarkRed;
|
||||
resultingColor.A = 0.7f;
|
||||
break;
|
||||
}
|
||||
|
||||
return resultingColor;
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return false;
|
||||
|
||||
_gasTileOverlay ??= _entManager.System<GasTileOverlaySystem>();
|
||||
if (_gasTileOverlay == null)
|
||||
return false;
|
||||
|
||||
var target = args.Viewport.RenderTarget;
|
||||
|
||||
if (_temperatureTarget?.Texture.Size != target.Size)
|
||||
{
|
||||
_temperatureTarget?.Dispose();
|
||||
_temperatureTarget = _clyde.CreateRenderTarget(
|
||||
target.Size,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: nameof(GasTileDangerousTemperatureOverlay));
|
||||
}
|
||||
|
||||
var drawHandle = args.WorldHandle;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var worldAABB = args.WorldAABB;
|
||||
var mapId = args.MapId;
|
||||
var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
|
||||
|
||||
var anyGasDrawn = false;
|
||||
List<Entity<MapGridComponent>> grids = new();
|
||||
|
||||
drawHandle.RenderInRenderTarget(_temperatureTarget,
|
||||
() =>
|
||||
{
|
||||
grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids);
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
if (!_overlayQuery.TryGetComponent(grid.Owner, out var comp))
|
||||
continue;
|
||||
|
||||
var gridTileSizeVec = grid.Comp.TileSizeVector;
|
||||
var gridTileCenterVec = grid.Comp.TileSizeHalfVector;
|
||||
var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
|
||||
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
|
||||
|
||||
drawHandle.SetTransform(gridEntToViewportLocal);
|
||||
|
||||
var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner);
|
||||
var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
|
||||
|
||||
var localBounds = new Box2i(
|
||||
(int)MathF.Floor(floatBounds.Left),
|
||||
(int)MathF.Floor(floatBounds.Bottom),
|
||||
(int)MathF.Ceiling(floatBounds.Right),
|
||||
(int)MathF.Ceiling(floatBounds.Top));
|
||||
|
||||
foreach (var chunk in comp.Chunks.Values)
|
||||
{
|
||||
var enumerator = new GasChunkEnumerator(chunk);
|
||||
while (enumerator.MoveNext(out var tileGas))
|
||||
{
|
||||
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
|
||||
if (!localBounds.Contains(tilePosition))
|
||||
continue;
|
||||
|
||||
var gasColor = _colorCache[tileGas.ByteGasTemperature.Value];
|
||||
|
||||
if (gasColor.A <= 0f)
|
||||
continue;
|
||||
|
||||
anyGasDrawn = true;
|
||||
|
||||
drawHandle.DrawRect(
|
||||
Box2.CenteredAround(tilePosition + gridTileCenterVec, gridTileSizeVec),
|
||||
gasColor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Color(0, 0, 0, 0));
|
||||
|
||||
drawHandle.SetTransform(Matrix3x2.Identity);
|
||||
|
||||
if (!anyGasDrawn)
|
||||
{
|
||||
_temperatureTarget?.Dispose();
|
||||
_temperatureTarget = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (_temperatureTarget is null)
|
||||
return;
|
||||
|
||||
args.WorldHandle.DrawTextureRect(_temperatureTarget.Texture, args.WorldBounds);
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
_temperatureTarget?.Dispose();
|
||||
_temperatureTarget = null;
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,6 @@ public sealed partial class ContentAudioSystem
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly CombatModeSystem _combatModeSystem = default!; //CLIENT ONE. WHY ARE THERE 3???
|
||||
[Dependency] private readonly IPrototypeManager _protMan = default!;
|
||||
[Dependency] private readonly SpaceBiomeSystem _spaceBiome = default!;
|
||||
|
||||
//options menu ---
|
||||
private static float _volumeSliderAmbient;
|
||||
|
||||
@@ -8,7 +8,6 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Serilog;
|
||||
|
||||
namespace Content.Client.Cargo.UI;
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ public sealed class CharacterInfoSystem : EntitySystem
|
||||
private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var entity = GetEntity(msg.NetEntity);
|
||||
// Mono - fix
|
||||
if (TerminatingOrDeleted(entity))
|
||||
return;
|
||||
|
||||
var data = new CharacterData(entity, msg.JobTitle, msg.Objectives, msg.Briefing, Name(entity));
|
||||
|
||||
OnCharacterUpdate?.Invoke(data);
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" Name="GeneralCategoryTexture"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" Name="HandsCategoryTexture"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ using Content.Shared.Speech;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Chat.UI;
|
||||
|
||||
@@ -86,6 +88,9 @@ public sealed partial class EmotesMenu : RadialMenu
|
||||
continue;
|
||||
AddEmoteClickAction(container);
|
||||
}
|
||||
|
||||
GeneralCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi"), "icon"));
|
||||
HandsCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi"), "icon"));
|
||||
}
|
||||
|
||||
private void AddEmoteClickAction(RadialContainer container)
|
||||
|
||||
@@ -6,7 +6,6 @@ using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
@@ -14,6 +13,7 @@ using System.Numerics;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.Graphics;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
{
|
||||
@@ -24,6 +24,8 @@ namespace Content.Client.Chemistry.UI
|
||||
public sealed partial class ChemMasterWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
private readonly SpriteSystem _sprite;
|
||||
public event Action<BaseButton.ButtonEventArgs, ReagentButton>? OnReagentButtonPressed;
|
||||
public readonly Button[] PillTypeButtons;
|
||||
|
||||
@@ -37,6 +39,7 @@ namespace Content.Client.Chemistry.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
|
||||
// Pill type selection buttons, in total there are 20 pills.
|
||||
// Pill rsi file should have states named as pill1, pill2, and so on.
|
||||
@@ -67,9 +70,9 @@ namespace Content.Client.Chemistry.UI
|
||||
|
||||
// Generate buttons textures
|
||||
var specifier = new SpriteSpecifier.Rsi(resourcePath, "pill" + (i + 1));
|
||||
TextureRect pillTypeTexture = new TextureRect
|
||||
var pillTypeTexture = new TextureRect
|
||||
{
|
||||
Texture = specifier.Frame0(),
|
||||
Texture = _sprite.Frame0(specifier),
|
||||
TextureScale = new Vector2(1.75f, 1.75f),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered,
|
||||
};
|
||||
@@ -249,7 +252,7 @@ namespace Content.Client.Chemistry.UI
|
||||
ChemMasterSortingType.Latest => Loc.GetString("chem-master-window-sort-type-latest"),
|
||||
_ => Loc.GetString("chem-master-window-sort-type-none")
|
||||
};
|
||||
|
||||
|
||||
OutputBufferDraw.Pressed = state.DrawSource == ChemMasterDrawSource.Internal;
|
||||
OutputBeakerDraw.Pressed = state.DrawSource == ChemMasterDrawSource.External;
|
||||
|
||||
@@ -434,7 +437,7 @@ namespace Content.Client.Chemistry.UI
|
||||
get => LabelLineEdit.Text;
|
||||
set => LabelLineEdit.Text = value;
|
||||
}
|
||||
|
||||
|
||||
private void SetBufferText(FixedPoint2? volume, string text)
|
||||
{
|
||||
BufferCurrentVolume.Text = $" {volume ?? FixedPoint2.Zero}u";
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>..\bin\Content.Client\</OutputPath>
|
||||
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nett" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
<PackageReference Include="Pidgin" />
|
||||
<PackageReference Include="Robust.Shared.AuthLib" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -34,7 +35,7 @@ namespace Content.Client.Cooldown
|
||||
if (Progress >= 0f)
|
||||
{
|
||||
var hue = (5f / 18f) * lerp;
|
||||
color = Color.FromHsv((hue, 0.75f, 0.75f, 0.50f));
|
||||
color = Color.FromHsv(new Vector4(hue, 0.75f, 0.75f, 0.50f));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Disposal;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Disposal;
|
||||
using Content.Shared.Disposal.Unit;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -36,39 +36,37 @@ public sealed partial class DocumentParsingManager
|
||||
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
|
||||
.Bind(tag => _tagControlParsers[tag]);
|
||||
|
||||
// Frontier: comment parser
|
||||
Parser<char, Unit> whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
|
||||
var whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
|
||||
|
||||
_controlParser = OneOf(_tagParser, TryHeaderControl, TryListControl, TextControlParser)
|
||||
.Before(whitespaceAndCommentParser);
|
||||
// End Frontier
|
||||
|
||||
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
|
||||
{
|
||||
_tagControlParsers.Add(typ.Name, CreateTagControlParser(typ.Name, typ, _sandboxHelper));
|
||||
}
|
||||
|
||||
ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many()); // Frontier: SkipWhitespaces<whitespaceAndCommentParser
|
||||
ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many());
|
||||
|
||||
_sawmill = Logger.GetSawmill("Guidebook");
|
||||
}
|
||||
|
||||
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
|
||||
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId)
|
||||
{
|
||||
if (!_prototype.TryIndex(entryId, out var entry))
|
||||
if (!_prototype.Resolve(entryId, out var entry))
|
||||
return false;
|
||||
|
||||
using var file = _resourceManager.ContentFileReadText(entry.Text);
|
||||
return TryAddMarkup(control, file.ReadToEnd(), log);
|
||||
return TryAddMarkup(control, file.ReadToEnd());
|
||||
}
|
||||
|
||||
public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true)
|
||||
public bool TryAddMarkup(Control control, GuideEntry entry)
|
||||
{
|
||||
using var file = _resourceManager.ContentFileReadText(entry.Text);
|
||||
return TryAddMarkup(control, file.ReadToEnd(), log);
|
||||
return TryAddMarkup(control, file.ReadToEnd());
|
||||
}
|
||||
|
||||
public bool TryAddMarkup(Control control, string text, bool log = true)
|
||||
public bool TryAddMarkup(Control control, string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed partial class DocumentParsingManager
|
||||
}
|
||||
|
||||
msg.Pop();
|
||||
rt.SetMessage(msg);
|
||||
rt.SetMessage(msg, tagsAllowed: null);
|
||||
return rt;
|
||||
},
|
||||
TextParser)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
|
||||
{
|
||||
TitleLabel.Text = title;
|
||||
if (markup)
|
||||
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
|
||||
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
|
||||
else
|
||||
Content.SetMessage(text);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Content.Client.Info
|
||||
}
|
||||
public void SetInfoBlob(string markup)
|
||||
{
|
||||
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
|
||||
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Items.Systems;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Hands;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Prometheus;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Nyanotrasen.Kitchen.UI;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="TitleLabel" Access="Public" />
|
||||
<Label Name="ExampleLabel" Access="Public" />
|
||||
<ColorSelectorSliders Name="Slider" Access="Public" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Client.Options.UI;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Options.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Standard UI control used for color sliders in the options menu. Intended for use with <see cref="OptionsTabControlRow"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="OptionsTabControlRow.AddOptionColorSlider"/>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class OptionColorSlider : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// The text describing what this slider affects.
|
||||
/// </summary>
|
||||
public string? Title
|
||||
{
|
||||
get => TitleLabel.Text;
|
||||
set => TitleLabel.Text = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The example text showing the current color of the slider.
|
||||
/// </summary>
|
||||
public string? Example
|
||||
{
|
||||
get => ExampleLabel.Text;
|
||||
set => ExampleLabel.Text = value;
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,19 @@ public sealed partial class OptionsTabControlRow : Control
|
||||
return AddOption(new OptionSliderIntCVar(this, _cfg, cVar, slider, min, max, format ?? FormatInt));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a color slider option, backed by a simple string CVar.
|
||||
/// </summary>
|
||||
/// <param name="cVar">The CVar represented by the slider.</param>
|
||||
/// <param name="slider">The UI control for the option.</param>
|
||||
/// <returns>The option instance backing the added option.</returns>
|
||||
public OptionColorSliderCVar AddOptionColorSlider(
|
||||
CVarDef<string> cVar,
|
||||
OptionColorSlider slider)
|
||||
{
|
||||
return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a drop-down option, backed by a CVar.
|
||||
/// </summary>
|
||||
@@ -518,6 +531,54 @@ public sealed class OptionSliderFloatCVar : BaseOptionCVar<float>
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OptionColorSliderCVar : BaseOptionCVar<string>
|
||||
{
|
||||
private readonly OptionColorSlider _slider;
|
||||
|
||||
protected override string Value
|
||||
{
|
||||
get => _slider.Slider.Color.ToHex();
|
||||
set
|
||||
{
|
||||
_slider.Slider.Color = Color.FromHex(value);
|
||||
UpdateLabelColor();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of this type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
|
||||
/// such as <see cref="OptionsTabControlRow.AddOptionPercentSlider"/> instead of instantiating this type directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="controller">The control row that owns this option.</param>
|
||||
/// <param name="cfg">The configuration manager to get and set values from.</param>
|
||||
/// <param name="cVar">The CVar that is being controlled by this option.</param>
|
||||
/// <param name="slider">The UI control for the option.</param>
|
||||
public OptionColorSliderCVar(
|
||||
OptionsTabControlRow controller,
|
||||
IConfigurationManager cfg,
|
||||
CVarDef<string> cVar,
|
||||
OptionColorSlider slider) : base(controller, cfg, cVar)
|
||||
{
|
||||
_slider = slider;
|
||||
|
||||
slider.Slider.OnColorChanged += _ =>
|
||||
{
|
||||
ValueChanged();
|
||||
UpdateLabelColor();
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateLabelColor()
|
||||
{
|
||||
_slider.ExampleLabel.FontColorOverride = Color.FromHex(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a CVar option that simply corresponds with an integer <see cref="OptionSlider"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
|
||||
<CheckBox Name="AutoFillHighlightsCheckBox" Text="{Loc 'ui-options-auto-fill-highlights'}" />
|
||||
<ui:OptionColorSlider Name="HighlightsColorSlider"
|
||||
Title="{Loc 'ui-options-highlights-color'}"
|
||||
Example="{Loc 'ui-options-highlights-color-example'}"/>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||
|
||||
@@ -20,6 +20,8 @@ public sealed partial class AccessibilityTab : Control
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
|
||||
Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
|
||||
Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Content.Client.Atmos.Overlays;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class ThermalSightOverlaySystem : EquipmentHudSystem<ThermalSightComponent>
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
|
||||
private GasTileDangerousTemperatureOverlay _temperatureOverlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_temperatureOverlay = new();
|
||||
}
|
||||
|
||||
protected override void UpdateInternal(RefreshEquipmentHudEvent<ThermalSightComponent> component)
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
_overlayMan.AddOverlay(_temperatureOverlay);
|
||||
}
|
||||
|
||||
protected override void DeactivateInternal()
|
||||
{
|
||||
base.DeactivateInternal();
|
||||
|
||||
_overlayMan.RemoveOverlay(_temperatureOverlay);
|
||||
}
|
||||
}
|
||||
@@ -98,10 +98,13 @@ public sealed class ParallaxManager : IParallaxManager
|
||||
}
|
||||
else
|
||||
{
|
||||
layers = await Task.WhenAll(
|
||||
// Explicitly allocate params array to avoid sandbox violation since C# 14.
|
||||
var tasks = new[]
|
||||
{
|
||||
LoadParallaxLayers(parallaxPrototype.Layers, loadedLayers, cancel),
|
||||
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel)
|
||||
);
|
||||
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel),
|
||||
};
|
||||
layers = await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Power.APC.UI
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Content.Client.Power;
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
// Mono
|
||||
using Content.Shared._Mono.Shuttles;
|
||||
|
||||
namespace Content.Client.Shuttles.BUI;
|
||||
|
||||
[UsedImplicitly]
|
||||
@@ -25,6 +28,7 @@ public sealed partial class ShuttleConsoleBoundUserInterface : BoundUserInterfac
|
||||
|
||||
_window.RequestFTL += OnFTLRequest;
|
||||
_window.RequestBeaconFTL += OnFTLBeaconRequest;
|
||||
_window.RequestAutopilot += OnAutopilotRequest; // Mono
|
||||
_window.DockRequest += OnDockRequest;
|
||||
_window.UndockRequest += OnUndockRequest;
|
||||
_window.UndockAllRequest += OnUndockAllRequest;
|
||||
@@ -78,6 +82,16 @@ public sealed partial class ShuttleConsoleBoundUserInterface : BoundUserInterfac
|
||||
});
|
||||
}
|
||||
|
||||
// Mono
|
||||
private void OnAutopilotRequest(MapCoordinates obj, Angle angle)
|
||||
{
|
||||
SendMessage(new ShuttleConsoleAutopilotPositionMessage()
|
||||
{
|
||||
Coordinates = obj,
|
||||
Angle = angle,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.UserInterface;
|
||||
|
||||
namespace Content.Client.Shuttles.Systems;
|
||||
|
||||
@@ -17,4 +18,17 @@ public sealed class ShuttleConsoleLockSystem : SharedShuttleConsoleLockSystem
|
||||
// Prediction only
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client extension of OnUIOpenAttempt. Prevents UI flashing open on client.
|
||||
/// </summary>
|
||||
protected override void OnUIOpenAttempt(EntityUid uid,
|
||||
ShuttleConsoleLockComponent component,
|
||||
ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
base.OnUIOpenAttempt(uid, component, args);
|
||||
|
||||
if(Timing.IsFirstTimePredicted && args.Cancelled)
|
||||
Popup.PopupEntity(Loc.GetString("shuttle-console-locked"), uid, args.User);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,12 @@
|
||||
Text="{controls:Loc 'shuttle-console-ftl-button'}"
|
||||
TextAlign="Center"
|
||||
StyleClasses="ButtonSquare"/>
|
||||
<!-- Mono - Autopilot -->
|
||||
<controls:Button Name="MapAutopilotButton"
|
||||
ToggleMode="True"
|
||||
Text="{controls:Loc 'shuttle-console-autopilot-button'}"
|
||||
TextAlign="Center"
|
||||
StyleClasses="ButtonSquare"/>
|
||||
<controls:Button Name="MapRebuildButton"
|
||||
Text="{controls:Loc 'shuttle-console-map-rebuild'}"
|
||||
TextAlign="Center"
|
||||
|
||||
@@ -51,6 +51,9 @@ public sealed partial class MapScreen : BoxContainer
|
||||
private TimeSpan _pingCooldown = TimeSpan.FromSeconds(3);
|
||||
private TimeSpan _nextMapDequeue;
|
||||
|
||||
// Mono
|
||||
private bool _autopilotTargeting = false;
|
||||
|
||||
private float _minMapDequeue = 0.05f;
|
||||
private float _maxMapDequeue = 0.25f;
|
||||
|
||||
@@ -59,6 +62,9 @@ public sealed partial class MapScreen : BoxContainer
|
||||
public event Action<MapCoordinates, Angle>? RequestFTL;
|
||||
public event Action<NetEntity, Angle>? RequestBeaconFTL;
|
||||
|
||||
// Mono
|
||||
public event Action<MapCoordinates, Angle>? RequestAutopilot;
|
||||
|
||||
private readonly Dictionary<MapId, BoxContainer> _mapHeadings = new();
|
||||
private readonly Dictionary<MapId, List<IMapObject>> _mapObjects = new();
|
||||
private readonly List<(MapId mapId, IMapObject mapobj)> _pendingMapObjects = new();
|
||||
@@ -86,6 +92,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
OnVisibilityChanged += OnVisChange;
|
||||
|
||||
MapFTLButton.OnToggled += FtlPreviewToggled;
|
||||
MapAutopilotButton.OnToggled += AutopilotPreviewToggled; // Mono
|
||||
|
||||
_ftlStyle = new StyleBoxFlat(Color.LimeGreen);
|
||||
FTLBar.ForegroundStyleBoxOverride = _ftlStyle;
|
||||
@@ -93,7 +100,15 @@ public sealed partial class MapScreen : BoxContainer
|
||||
// Just pass it on up.
|
||||
MapRadar.RequestFTL += (coords, angle) =>
|
||||
{
|
||||
RequestFTL?.Invoke(coords, angle);
|
||||
if (_autopilotTargeting) // Mono
|
||||
{
|
||||
RequestAutopilot?.Invoke(coords, angle);
|
||||
SetTargeting(false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestFTL?.Invoke(coords, angle);
|
||||
}
|
||||
};
|
||||
|
||||
MapRadar.RequestBeaconFTL += (ent, angle) =>
|
||||
@@ -192,12 +207,31 @@ public sealed partial class MapScreen : BoxContainer
|
||||
|
||||
private void FtlPreviewToggled(BaseButton.ButtonToggledEventArgs obj)
|
||||
{
|
||||
MapRadar.FtlMode = obj.Pressed;
|
||||
SetTargeting(obj.Pressed, false);
|
||||
}
|
||||
|
||||
// When FTL button is toggled, disable the ShowFTLRangeOnly mode
|
||||
if (obj.Pressed)
|
||||
// Mono
|
||||
private void AutopilotPreviewToggled(BaseButton.ButtonToggledEventArgs obj)
|
||||
{
|
||||
SetTargeting(obj.Pressed, true);
|
||||
}
|
||||
|
||||
// Mono
|
||||
private void SetTargeting(bool pressed, bool isAutopilot)
|
||||
{
|
||||
if (pressed)
|
||||
{
|
||||
MapRadar.FtlMode = true;
|
||||
MapRadar.ShowFTLRangeOnly = false;
|
||||
MapRadar.ShowFTLRange = !isAutopilot;
|
||||
MapRadar.NoFTLRange = isAutopilot;
|
||||
MapFTLButton.Pressed = !isAutopilot;
|
||||
MapAutopilotButton.Pressed = isAutopilot;
|
||||
_autopilotTargeting = isAutopilot;
|
||||
}
|
||||
else
|
||||
{
|
||||
MapRadar.FtlMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +390,7 @@ public sealed partial class MapScreen : BoxContainer
|
||||
var name = hideLabel ?
|
||||
detectionLevel == DetectionLevel.PartialDetected ?
|
||||
Loc.GetString($"shuttle-console-signature-infrared")
|
||||
: Loc.GetString($"shuttle-console-signature-unknown")
|
||||
: _detection.HandleUnknownMassLabel(grid.Owner)
|
||||
: _entManager.GetComponent<MetaDataComponent>(grid.Owner).EntityName;
|
||||
|
||||
var gridObj = new GridMapObject()
|
||||
|
||||
@@ -62,13 +62,13 @@ public sealed partial class NavScreen : BoxContainer
|
||||
!string.IsNullOrEmpty(companyComp.CompanyName))
|
||||
{
|
||||
// Try to match the company ID directly
|
||||
if (companyComp.CompanyName.Contains(text, StringComparison.OrdinalIgnoreCase))
|
||||
if (companyComp.CompanyName.Id.Contains(text, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to match company name from prototype
|
||||
if (_prototypeManager.TryIndex<CompanyPrototype>(
|
||||
if (_prototypeManager.TryIndex(
|
||||
companyComp.CompanyName, out var prototype) &&
|
||||
prototype.Name.Contains(text, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -20,6 +20,9 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
|
||||
public event Action<MapCoordinates, Angle>? RequestFTL;
|
||||
public event Action<NetEntity, Angle>? RequestBeaconFTL;
|
||||
|
||||
// Mono
|
||||
public event Action<MapCoordinates, Angle>? RequestAutopilot;
|
||||
|
||||
public event Action<NetEntity, NetEntity>? DockRequest;
|
||||
public event Action<NetEntity>? UndockRequest;
|
||||
public event Action<List<NetEntity>>? UndockAllRequest;
|
||||
@@ -55,6 +58,12 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
|
||||
RequestBeaconFTL?.Invoke(ent, angle);
|
||||
};
|
||||
|
||||
// Mono
|
||||
MapContainer.RequestAutopilot += (coords, angle) =>
|
||||
{
|
||||
RequestAutopilot?.Invoke(coords, angle);
|
||||
};
|
||||
|
||||
DockContainer.DockRequest += (entity, netEntity) =>
|
||||
{
|
||||
DockRequest?.Invoke(entity, netEntity);
|
||||
@@ -64,12 +73,12 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
|
||||
{
|
||||
UndockRequest?.Invoke(entity);
|
||||
};
|
||||
|
||||
|
||||
DockContainer.UndockAllRequest += dockEntities =>
|
||||
{
|
||||
UndockAllRequest?.Invoke(dockEntities);
|
||||
};
|
||||
|
||||
|
||||
DockContainer.ToggleFTLLockRequest += (dockEntities, enabled) =>
|
||||
{
|
||||
ToggleFTLLockRequest?.Invoke(dockEntities, enabled);
|
||||
|
||||
@@ -57,6 +57,16 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
|
||||
/// </summary>
|
||||
public bool ShowFTLRangeOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Mono - Whether to show FTL range at all.
|
||||
/// </summary>
|
||||
public bool ShowFTLRange = true;
|
||||
|
||||
/// <summary>
|
||||
/// Mono - Whether to ignore FTL obstructions and range for preview display.
|
||||
/// </summary>
|
||||
public bool NoFTLRange = false;
|
||||
|
||||
private Angle _ftlAngle;
|
||||
|
||||
/// <summary>
|
||||
@@ -275,7 +285,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
|
||||
|
||||
// Draw our FTL range + no FTL zones
|
||||
// Do it up here because we want this layered below most things.
|
||||
if (FtlMode || ShowFTLRangeOnly)
|
||||
if ((FtlMode || ShowFTLRangeOnly) && ShowFTLRange) // Mono
|
||||
{
|
||||
if (EntManager.TryGetComponent<TransformComponent>(_shuttleEntity, out var shuttleXform))
|
||||
{
|
||||
@@ -413,7 +423,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
|
||||
var iffText = hideLabel ?
|
||||
detectionLevel == DetectionLevel.PartialDetected ?
|
||||
Loc.GetString($"shuttle-console-signature-infrared")
|
||||
: Loc.GetString($"shuttle-console-signature-unknown")
|
||||
: _detection.HandleUnknownMassLabel(grid.Owner)
|
||||
: _shuttles.GetIFFLabel(grid, self: true, component: iffComp);
|
||||
|
||||
if (string.IsNullOrEmpty(iffText))
|
||||
@@ -515,7 +525,8 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
|
||||
var mouseMapPos = InverseMapPosition(mouseLocalPos);
|
||||
|
||||
var ftlFree = (!beaconsOnly || foundBeacon != default) &&
|
||||
_shuttles.FTLFree(_shuttleEntity.Value, new EntityCoordinates(viewedMapUid, mouseMapPos), _ftlAngle, _viewportExclusions);
|
||||
_shuttles.FTLFree(_shuttleEntity.Value, new EntityCoordinates(viewedMapUid, mouseMapPos), _ftlAngle, _viewportExclusions)
|
||||
|| NoFTLRange; // Mono
|
||||
|
||||
var color = ftlFree ? Color.LimeGreen : Color.Magenta;
|
||||
|
||||
|
||||
@@ -51,6 +51,17 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
|
||||
private Dictionary<NetEntity, List<DockingPortState>> _docks = new();
|
||||
|
||||
// temporary buffers to avoid per-frame heap churn
|
||||
private readonly List<BlipData> _tempBlipDataList = new();
|
||||
private readonly HashSet<EntityUid> _visibleGridsSet = new();
|
||||
private static readonly Vector2[] RadarPosVertsCache =
|
||||
[
|
||||
new Vector2(0f, -2f),
|
||||
new Vector2(1f, 0f),
|
||||
new Vector2(0f, 2f),
|
||||
new Vector2(-1f, 0f),
|
||||
];
|
||||
|
||||
public bool ShowIFF { get; set; } = true;
|
||||
public bool ShowIFFShuttles { get; set; } = true;
|
||||
public bool ShowDocks { get; set; } = true;
|
||||
@@ -341,14 +352,12 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
}
|
||||
|
||||
// Draw radar position on the station
|
||||
const float radarVertRadius = 2f;
|
||||
var radarPosVerts = new Vector2[]
|
||||
// Mono - use precalculated verts and scale each point rather than allocating a fresh array
|
||||
var radarPosVerts = new Vector2[RadarPosVertsCache.Length];
|
||||
for (var i = 0; i < radarPosVerts.Length; i++)
|
||||
{
|
||||
ScalePosition(new Vector2(0f, -radarVertRadius)),
|
||||
ScalePosition(new Vector2(radarVertRadius / 2f, 0f)),
|
||||
ScalePosition(new Vector2(0f, radarVertRadius)),
|
||||
ScalePosition(new Vector2(radarVertRadius / -2f, 0f)),
|
||||
};
|
||||
radarPosVerts[i] = ScalePosition(RadarPosVertsCache[i]);
|
||||
}
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, radarPosVerts, Color.Lime);
|
||||
|
||||
@@ -358,10 +367,10 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
|
||||
|
||||
// Frontier - collect blip location data outside foreach - more changes ahead
|
||||
var blipDataList = new List<BlipData>();
|
||||
// Mono edited: Frontier - collect blip location data outside foreach - more changes ahead
|
||||
_tempBlipDataList.Clear();
|
||||
|
||||
var visibleGrids = new HashSet<EntityUid>();
|
||||
_visibleGridsSet.Clear();
|
||||
|
||||
// Draw other grids... differently
|
||||
foreach (var grid in _grids)
|
||||
@@ -385,8 +394,7 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
continue;
|
||||
|
||||
if (!blipOnly)
|
||||
visibleGrids.Add(grid);
|
||||
|
||||
_visibleGridsSet.Add(grid);
|
||||
var curGridToWorld = _transform.GetWorldMatrix(gUid);
|
||||
var curGridToView = curGridToWorld * worldToView;
|
||||
|
||||
@@ -402,7 +410,7 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
var labelName = noLabel ? null : hideLabel ?
|
||||
detectionLevel == DetectionLevel.PartialDetected ?
|
||||
Loc.GetString($"shuttle-console-signature-infrared")
|
||||
: Loc.GetString($"shuttle-console-signature-unknown")
|
||||
: _detection.HandleUnknownMassLabel(grid.Owner)
|
||||
: _shuttles.GetIFFLabel(grid, self: false, component: iff);
|
||||
|
||||
var shouldDrawIFF = ShowIFF && labelName != null;
|
||||
@@ -562,12 +570,12 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
}
|
||||
}
|
||||
|
||||
NfAddBlipToList(blipDataList, isOutsideRadarCircle, uiPosition, uiXCentre, uiYCentre, labelColor, hideLabel ? default : gUid); // Frontier code
|
||||
NfAddBlipToList(_tempBlipDataList, isOutsideRadarCircle, uiPosition, uiXCentre, uiYCentre, labelColor, hideLabel ? default : gUid); // Frontier code
|
||||
// End Frontier: IFF drawing functions
|
||||
}
|
||||
|
||||
// Frontier Don't skip drawing blips if they're out of range.
|
||||
NfDrawBlips(handle, blipDataList);
|
||||
NfDrawBlips(handle, _tempBlipDataList);
|
||||
|
||||
// Detailed view
|
||||
var gridAABB = curGridToWorld.TransformBox(grid.Comp.LocalAABB);
|
||||
@@ -626,7 +634,7 @@ public partial class ShuttleNavControl : BaseShuttleControl // Mono
|
||||
if (blip.GridUid is { } grid)
|
||||
{
|
||||
// check detection if we're on a grid and that grid isn't our grid
|
||||
if (!visibleGrids.Contains(grid) && grid != ourGridId)
|
||||
if (!_visibleGridsSet.Contains(grid) && grid != ourGridId)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,22 @@ public sealed partial class BorgSelectTypeMenu : FancyWindow
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
foreach (var borgType in _prototypeManager.EnumeratePrototypes<BorgTypePrototype>().OrderBy(PrototypeName))
|
||||
// Goobstation: Customizable borgs sprites
|
||||
ConfirmTypeButton.OnPressed += ConfirmButtonPressed;
|
||||
HelpGuidebookIds = GuidebookEntries;
|
||||
|
||||
SubtypeSelection.SubtypeSelected += () =>
|
||||
ConfirmTypeButton.Disabled = false;
|
||||
}
|
||||
|
||||
// foreach (var borgType in _prototypeManager.EnumeratePrototypes<BorgTypePrototype>().OrderBy(PrototypeName))
|
||||
// Mono: Selectable borg whitelist
|
||||
public void Populate(IReadOnlyList<ProtoId<BorgTypePrototype>> whitelist)
|
||||
{
|
||||
IEnumerable<BorgTypePrototype> types = whitelist.Select(id => _prototypeManager.Index(id)).OrderBy(PrototypeName);
|
||||
foreach (var borgType in types)
|
||||
// Mono: Selectable borg whitelist end
|
||||
{
|
||||
// Goobstation-Start: Customizable borgs sprites
|
||||
var chassisList = new EntityPrototypeView
|
||||
{
|
||||
Scale = new Vector2(2, 2),
|
||||
@@ -49,13 +62,6 @@ public sealed partial class BorgSelectTypeMenu : FancyWindow
|
||||
};
|
||||
SelectionsContainer.AddChild(chassisList);
|
||||
}
|
||||
|
||||
ConfirmTypeButton.OnPressed += ConfirmButtonPressed;
|
||||
HelpGuidebookIds = GuidebookEntries;
|
||||
|
||||
|
||||
SubtypeSelection.SubtypeSelected += () =>
|
||||
ConfirmTypeButton.Disabled = false;
|
||||
// Goobstation-End: Customizable borgs sprites
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@ public sealed class BorgSelectTypeUserInterface : BoundUserInterface
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
//Mono: Selectable borg whitelist
|
||||
EntMan.TryGetComponent<BorgSwitchableTypeComponent>(Owner, out var comp);
|
||||
var whitelist = comp?.TypeWhitelist ?? [];
|
||||
|
||||
_menu = this.CreateWindow<BorgSelectTypeMenu>();
|
||||
_menu.Populate(whitelist); //Mono
|
||||
_menu.ConfirmedBorgType += (prototype, subtype) => SendPredictedMessage(new BorgSelectTypeMessage(prototype, subtype));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,18 +46,15 @@ public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
|
||||
{
|
||||
if (TryComp(entity, out SpriteComponent? sprite))
|
||||
{
|
||||
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Body, prototype.SpriteBodyState);
|
||||
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Light, prototype.SpriteBodyState);
|
||||
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteBodyState);
|
||||
|
||||
var rsiPath = SpriteSpecifierSerializer.TextureRoot / subtypePrototype.SpritePath;
|
||||
if (_resourceCache.TryGetResource<RSIResource>(rsiPath, out var resource))
|
||||
{
|
||||
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Body, resource.RSI);
|
||||
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Light, resource.RSI);
|
||||
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.LightStatus, resource.RSI);
|
||||
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Body, resource.RSI, prototype.SpriteBodyState);
|
||||
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Light, resource.RSI, prototype.SpriteBodyState);
|
||||
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.LightStatus, resource.RSI, prototype.SpriteToggleLightState);
|
||||
}
|
||||
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Body, prototype.SpriteBodyState);
|
||||
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteBodyState);
|
||||
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteToggleLightState);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Sprite;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Sprite;
|
||||
|
||||
public sealed class ScaleVisualsSystem : SharedScaleVisualsSystem
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ScaleVisualsComponent, AppearanceChangeEvent>(OnChangeData);
|
||||
}
|
||||
|
||||
private void OnChangeData(Entity<ScaleVisualsComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (!args.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
|
||||
args.Sprite == null) return;
|
||||
|
||||
// save the original scale
|
||||
ent.Comp.OriginalScale ??= args.Sprite.Scale;
|
||||
|
||||
var vecScale = (Vector2)scale;
|
||||
_sprite.SetScale((ent.Owner, args.Sprite), vecScale);
|
||||
}
|
||||
|
||||
// revert to the original scale
|
||||
protected override void ResetScale(Entity<ScaleVisualsComponent> ent)
|
||||
{
|
||||
base.ResetScale(ent);
|
||||
|
||||
if (ent.Comp.OriginalScale != null)
|
||||
_sprite.SetScale(ent.Owner, ent.Comp.OriginalScale.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Client.CharacterInfo;
|
||||
using static Content.Client.CharacterInfo.CharacterInfoSystem;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat;
|
||||
|
||||
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
|
||||
|
||||
private static readonly Regex StartDoubleQuote = new("\"$");
|
||||
private static readonly Regex EndDoubleQuote = new("^\"|(?<=^@)\"");
|
||||
private static readonly Regex StartAtSign = new("^@");
|
||||
|
||||
|
||||
private readonly List<string> _highlights = new();
|
||||
private readonly List<string> _autoFillRawKeywords = new();
|
||||
|
||||
private string? _highlightsColor;
|
||||
private bool _autoFillHighlightsEnabled;
|
||||
private bool _charInfoIsAttach = false;
|
||||
private string? _currentCharacterName;
|
||||
|
||||
|
||||
public event Action<string>? HighlightsUpdated;
|
||||
public event Action<string>? AutoFillUpdated;
|
||||
|
||||
private void InitializeHighlights()
|
||||
{
|
||||
_config.OnValueChanged(CCVars.ChatAutoFillHighlights, (value) => { _autoFillHighlightsEnabled = value; }, true);
|
||||
_config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
|
||||
|
||||
var cvar = _config.GetCVar(CCVars.ChatHighlights);
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(CharacterInfoSystem system)
|
||||
{
|
||||
system.OnCharacterUpdate += OnCharacterUpdated;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(CharacterInfoSystem system)
|
||||
{
|
||||
system.OnCharacterUpdate -= OnCharacterUpdated;
|
||||
}
|
||||
|
||||
private void UpdateAutoFillHighlights()
|
||||
{
|
||||
_currentCharacterName = null;
|
||||
_autoFillRawKeywords.Clear();
|
||||
_highlights.Clear();
|
||||
AutoFillUpdated?.Invoke("");
|
||||
HighlightsUpdated?.Invoke("");
|
||||
|
||||
_charInfoIsAttach = true;
|
||||
_characterInfo.RequestCharacterInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the user's custom keywords for the given character into the CVar,
|
||||
/// then rebuilds the active highlight list.
|
||||
/// Called when the user submits keywords from the popup.
|
||||
/// </summary>
|
||||
public void UpdateHighlights(string customKeywords)
|
||||
{
|
||||
if (_currentCharacterName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveCustomKeywords(_currentCharacterName, customKeywords);
|
||||
RebuildHighlightsList(customKeywords);
|
||||
HighlightsUpdated?.Invoke(customKeywords);
|
||||
}
|
||||
|
||||
private void OnCharacterUpdated(CharacterData data)
|
||||
{
|
||||
|
||||
if (!_charInfoIsAttach)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (_, job, _, _, entityName) = data;
|
||||
_currentCharacterName = entityName;
|
||||
|
||||
|
||||
_autoFillRawKeywords.Clear();
|
||||
var autoFillDisplay = "";
|
||||
|
||||
if (_autoFillHighlightsEnabled)
|
||||
{
|
||||
|
||||
// Special rules for names can be added here
|
||||
|
||||
var nameHighlights = "@" + entityName;
|
||||
|
||||
// Split on spaces first (handles any number of space-separated words).
|
||||
if (nameHighlights.Contains(' '))
|
||||
nameHighlights = nameHighlights.Replace(" ", "\n@");
|
||||
|
||||
// Handle hyphenated tokens: single hyphen splits normally (e.g. "First-Last"),
|
||||
// multiple hyphens use the lizard rule — keep only first and last part (e.g. "Eats-The-Food" → @Eats @Food).
|
||||
var dashParts = nameHighlights.Split('-');
|
||||
if (dashParts.Length == 2)
|
||||
nameHighlights = nameHighlights.Replace("-", "\n@");
|
||||
else if (dashParts.Length > 2)
|
||||
nameHighlights = dashParts[0] + "\n@" + dashParts[^1];
|
||||
|
||||
|
||||
foreach (var token in nameHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
_autoFillRawKeywords.Add(token);
|
||||
}
|
||||
|
||||
var jobKey = job.Replace(' ', '-').ToLower();
|
||||
|
||||
if (_loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
|
||||
{
|
||||
foreach (var jobWord in jobMatches.Replace(", ", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
_autoFillRawKeywords.Add(jobWord);
|
||||
}
|
||||
autoFillDisplay = string.Join("\n", _autoFillRawKeywords);
|
||||
}
|
||||
|
||||
var customKeywords = LoadCustomKeywords(entityName);
|
||||
|
||||
RebuildHighlightsList(customKeywords);
|
||||
|
||||
AutoFillUpdated?.Invoke(autoFillDisplay);
|
||||
HighlightsUpdated?.Invoke(customKeywords);
|
||||
_charInfoIsAttach = false;
|
||||
}
|
||||
|
||||
|
||||
private string LoadCustomKeywords(string characterName)
|
||||
{
|
||||
var cvar = _config.GetCVar(CCVars.ChatHighlights);
|
||||
if (string.IsNullOrEmpty(cvar))
|
||||
return "";
|
||||
|
||||
// Shouldn't be needed as no-one should have data by the time this rolls out but we never know
|
||||
if (!cvar.Contains("||"))
|
||||
{
|
||||
return cvar;
|
||||
}
|
||||
|
||||
foreach (var line in cvar.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var sep = line.IndexOf("||", StringComparison.Ordinal);
|
||||
if (sep < 0)
|
||||
continue;
|
||||
|
||||
if (line[..sep] == characterName)
|
||||
return DecodeKeywords(line[(sep + 2)..]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
private void SaveCustomKeywords(string characterName, string customKeywords)
|
||||
{
|
||||
var cvar = _config.GetCVar(CCVars.ChatHighlights);
|
||||
|
||||
var entries = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrEmpty(cvar) && cvar.Contains("||"))
|
||||
{
|
||||
foreach (var line in cvar.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var sep = line.IndexOf("||", StringComparison.Ordinal);
|
||||
if (sep < 0)
|
||||
continue;
|
||||
entries[line[..sep]] = line[(sep + 2)..];
|
||||
}
|
||||
}
|
||||
|
||||
entries[characterName] = EncodeKeywords(customKeywords);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var (name, encoded) in entries)
|
||||
sb.Append(name).Append("||").Append(encoded).Append('\n');
|
||||
|
||||
var newValue = sb.ToString().TrimEnd('\n');
|
||||
_config.SetCVar(CCVars.ChatHighlights, newValue);
|
||||
_config.SaveToFile();
|
||||
}
|
||||
|
||||
private static string EncodeKeywords(string raw)
|
||||
{
|
||||
return raw
|
||||
.Replace(@"\", @"\\")
|
||||
.Replace("|", @"\|")
|
||||
.Replace("\n", @"\n");
|
||||
}
|
||||
|
||||
private static string DecodeKeywords(string encoded)
|
||||
{
|
||||
var sb = new StringBuilder(encoded.Length);
|
||||
for (var i = 0; i < encoded.Length; i++)
|
||||
{
|
||||
if (encoded[i] == '\\' && i + 1 < encoded.Length)
|
||||
{
|
||||
switch (encoded[i + 1])
|
||||
{
|
||||
case 'n': sb.Append('\n'); i++; break;
|
||||
case '|': sb.Append('|'); i++; break;
|
||||
case '\\': sb.Append('\\'); i++; break;
|
||||
default: sb.Append(encoded[i]); break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(encoded[i]);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void RebuildHighlightsList(string customRaw)
|
||||
{
|
||||
_highlights.Clear();
|
||||
|
||||
foreach (var raw in _autoFillRawKeywords)
|
||||
_highlights.Add(ProcessKeyword(raw));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customRaw))
|
||||
{
|
||||
foreach (var raw in customRaw.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
_highlights.Add(ProcessKeyword(raw));
|
||||
}
|
||||
|
||||
_highlights.Sort((x, y) => y.Length.CompareTo(x.Length));
|
||||
}
|
||||
|
||||
private string ProcessKeyword(string raw)
|
||||
{
|
||||
// Replace every "\" to prevent "\n", "\0", etc. in regex.
|
||||
var keyword = raw.Replace(@"\", @"\\");
|
||||
|
||||
// Escape special regex characters.
|
||||
keyword = Regex.Escape(keyword);
|
||||
|
||||
// Re-escape "[" for markup-adjacent positions.
|
||||
keyword = keyword.Replace(@"\[", @"\\\[");
|
||||
|
||||
// words in double quotes will only be hightlighted as unique, not within another word
|
||||
if (keyword.Any(c => c == '"'))
|
||||
{
|
||||
keyword = StartDoubleQuote.Replace(keyword, "(?!\\w)");
|
||||
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
|
||||
}
|
||||
|
||||
// Make it so it doesn't hightlight your own name when you are talking
|
||||
keyword = StartAtSign.Replace(keyword, @"(?<=\[BubbleContent\].*)");
|
||||
|
||||
return keyword;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat;
|
||||
|
||||
public sealed class ChatUIController : UIController
|
||||
public sealed partial class ChatUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IChatManager _manager = default!;
|
||||
@@ -248,6 +248,8 @@ public sealed class ChatUIController : UIController
|
||||
|
||||
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
|
||||
|
||||
InitializeHighlights();
|
||||
|
||||
}
|
||||
|
||||
public void OnScreenLoad()
|
||||
@@ -434,6 +436,7 @@ public sealed class ChatUIController : UIController
|
||||
private void OnAttachedChanged(EntityUid uid)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
UpdateAutoFillHighlights();
|
||||
}
|
||||
|
||||
private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
|
||||
@@ -877,6 +880,11 @@ public sealed class ChatUIController : UIController
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var highlight in _highlights)
|
||||
{
|
||||
msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, highlight, "color", _highlightsColor);
|
||||
}
|
||||
|
||||
// Log all incoming chat to repopulate when filter is un-toggled
|
||||
if (!msg.HideChat)
|
||||
{
|
||||
@@ -912,7 +920,6 @@ public sealed class ChatUIController : UIController
|
||||
case ChatChannel.Dead:
|
||||
if (_ghost is not {IsGhost: true})
|
||||
break;
|
||||
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
<controls:ChannelFilterPopup
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls">
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
<PanelContainer Name="FilterPopupPanel" StyleClasses="BorderedWindowPanel">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Control MinSize="4 0"/>
|
||||
<BoxContainer Name="FilterVBox" MinWidth="110" Margin="0 10" Orientation="Vertical" SeparationOverride="4"/>
|
||||
<BoxContainer Name="HighlightsVBox" MinWidth="120" Margin="0 10" Orientation="Vertical" SeparationOverride="4">
|
||||
<BoxContainer Name="AutoFillVBox" Orientation="Vertical" SeparationOverride="2" Visible="False">
|
||||
<Label Text="{Loc 'hud-chatbox-autofill-highlights'}"/>
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#262736"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<RichTextLabel Name="AutoFillLabel" Margin="5 5"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<Label Text="{Loc 'hud-chatbox-highlights'}"/>
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#323446"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<TextEdit Name="HighlightEdit" MinHeight="150" Margin="5 5"/>
|
||||
</PanelContainer>
|
||||
<Button Name="HighlightButton" Text="{Loc 'hud-chatbox-highlights-button'}" ToolTip="{Loc 'hud-chatbox-highlights-tooltip'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:ChannelFilterPopup>
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Chat;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
@@ -30,10 +31,13 @@ public sealed partial class ChannelFilterPopup : Popup
|
||||
private readonly Dictionary<ChatChannel, ChannelFilterCheckbox> _filterStates = new();
|
||||
|
||||
public event Action<ChatChannel, bool>? OnChannelFilter;
|
||||
public event Action<string>? OnNewHighlights;
|
||||
|
||||
public ChannelFilterPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
HighlightButton.OnPressed += HighlightsEntered;
|
||||
HighlightEdit.Placeholder = new Rope.Leaf(Loc.GetString("hud-chatbox-highlights-placeholder"));
|
||||
}
|
||||
|
||||
public bool IsActive(ChatChannel channel)
|
||||
@@ -95,4 +99,36 @@ public sealed partial class ChannelFilterPopup : Popup
|
||||
if (_filterStates.TryGetValue(channel, out var checkbox))
|
||||
checkbox.UpdateUnreadCount(unread);
|
||||
}
|
||||
|
||||
// Highlight
|
||||
|
||||
/// <summary>
|
||||
/// Updates the read-only auto-fill section with name/job tokens.
|
||||
/// </summary>
|
||||
public void UpdateAutoFill(string autoFillKeywords)
|
||||
{
|
||||
AutoFillVBox.Visible = !string.IsNullOrEmpty(autoFillKeywords);
|
||||
if (string.IsNullOrEmpty(autoFillKeywords))
|
||||
return;
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
var lines = autoFillKeywords.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
if (i > 0) msg.PushNewline();
|
||||
msg.AddText(lines[i]);
|
||||
}
|
||||
AutoFillLabel.SetMessage(msg);
|
||||
}
|
||||
|
||||
public void UpdateHighlights(string highlights)
|
||||
{
|
||||
HighlightEdit.TextRope = new Rope.Leaf(highlights);
|
||||
}
|
||||
|
||||
private void HighlightsEntered(ButtonEventArgs _args)
|
||||
{
|
||||
OnNewHighlights?.Invoke(Rope.Collapse(HighlightEdit.TextRope));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,9 +47,12 @@ public partial class ChatBox : UIWidget
|
||||
ChatInput.Input.OnFocusExit += OnFocusExit;
|
||||
ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect;
|
||||
ChatInput.FilterButton.Popup.OnChannelFilter += OnChannelFilter;
|
||||
ChatInput.FilterButton.Popup.OnNewHighlights += OnNewHighlights;
|
||||
|
||||
_controller = UserInterfaceManager.GetUIController<ChatUIController>();
|
||||
_controller.MessageAdded += OnMessageAdded;
|
||||
_controller.HighlightsUpdated += OnHighlightsUpdated;
|
||||
_controller.AutoFillUpdated += OnAutoFillUpdated;
|
||||
_controller.RegisterChat(this);
|
||||
|
||||
// WD EDIT START
|
||||
@@ -133,6 +136,21 @@ public partial class ChatBox : UIWidget
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNewHighlights(string highlighs)
|
||||
{
|
||||
_controller.UpdateHighlights(highlighs);
|
||||
}
|
||||
|
||||
private void OnHighlightsUpdated(string highlights)
|
||||
{
|
||||
ChatInput.FilterButton.Popup.UpdateHighlights(highlights);
|
||||
}
|
||||
|
||||
private void OnAutoFillUpdated(string autoFillKeywords)
|
||||
{
|
||||
ChatInput.FilterButton.Popup.UpdateAutoFill(autoFillKeywords);
|
||||
}
|
||||
|
||||
public void AddLine(string message, Color color, int repeat = 0) // WD EDIT
|
||||
{
|
||||
var formatted = new FormattedMessage(4); // WD EDIT // specifying size beforehand smells like a useless microoptimisation, but i'll give them the benefit of doubt
|
||||
@@ -148,7 +166,7 @@ public partial class ChatBox : UIWidget
|
||||
("size", 8+sizeIncrease)
|
||||
));
|
||||
} // WD EDIT END
|
||||
Contents.AddMessage(formatted);
|
||||
Contents.AddMessage(formatted, tagsAllowed: null);
|
||||
}
|
||||
|
||||
public void Focus(ChatSelectChannel? channel = null)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Mobs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
|
||||
|
||||
// lerp
|
||||
var hue = 5f / 18f * progress;
|
||||
return Color.FromHsv((hue, 1f, 0.75f, 1f));
|
||||
return Color.FromHsv(new Vector4(hue, 1f, 0.75f, 1f));
|
||||
}
|
||||
|
||||
return InterpolateColorGaussian(Plasma, progress);
|
||||
|
||||
@@ -614,7 +614,7 @@ public sealed class StorageWindow : BaseWindow
|
||||
{
|
||||
marked.Add(cell);
|
||||
cell.ModulateSelfOverride = spotFree
|
||||
? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
|
||||
? Color.FromHsv(new Vector4(0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
|
||||
: Color.FromHex("#2222CC");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,11 @@ namespace Content.Client.Voting.UI
|
||||
var entry = _vote.Entries[i];
|
||||
if (_vote.DisplayVotes)
|
||||
{
|
||||
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
|
||||
// Mono
|
||||
if (entry.RealVotes != entry.Votes)
|
||||
_voteButtons[i].Text = Loc.GetString("ui-vote-button-weighted", ("text", entry.Text), ("votes", entry.Votes), ("realvotes", entry.RealVotes));
|
||||
else
|
||||
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -192,6 +192,7 @@ namespace Content.Client.Voting
|
||||
for (var i = 0; i < message.Options.Length; i++)
|
||||
{
|
||||
existingVote.Entries[i].Votes = message.Options[i].votes;
|
||||
existingVote.Entries[i].RealVotes = message.Options[i].realVotes; // Mono
|
||||
}
|
||||
|
||||
if (@new && _popupContainer != null)
|
||||
@@ -259,6 +260,7 @@ namespace Content.Client.Voting
|
||||
{
|
||||
public string Text { get; }
|
||||
public int Votes { get; set; }
|
||||
public int RealVotes; // Mono
|
||||
|
||||
public VoteEntry(string text)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared._Mono.Claws;
|
||||
|
||||
namespace Content.Client._Mono.Claws;
|
||||
|
||||
public sealed class ClawsSystem : SharedClawsSystem;
|
||||
@@ -32,6 +32,8 @@ public sealed class FireControlNavControl : ShuttleNavControl
|
||||
private FireControllableEntry[]? _controllables;
|
||||
private HashSet<NetEntity> _selectedWeapons = new();
|
||||
|
||||
private readonly Dictionary<NetEntity, Color> _blipColors = new();
|
||||
|
||||
// Add a limit to how often we update the cursor position to prevent network spam
|
||||
private float _lastCursorUpdateTime = 0f;
|
||||
private const float CursorUpdateInterval = 0.1f; // 10 updates per second
|
||||
@@ -77,9 +79,9 @@ public sealed class FireControlNavControl : ShuttleNavControl
|
||||
Matrix3x2.Invert(worldToView, out var viewToWorld);
|
||||
|
||||
var blips = _blips.GetCurrentBlips();
|
||||
var colors = new Dictionary<NetEntity, Color>();
|
||||
_blipColors.Clear();
|
||||
foreach (var blip in blips)
|
||||
colors[blip.NetUid] = blip.Config.Color;
|
||||
_blipColors[blip.NetUid] = blip.Config.Color;
|
||||
|
||||
if (_controllables != null)
|
||||
{
|
||||
@@ -100,7 +102,7 @@ public sealed class FireControlNavControl : ShuttleNavControl
|
||||
|
||||
var results = _physics.IntersectRay(xform.MapID, ray, direction.Length(), ignoredEnt: _coordinates?.EntityId);
|
||||
|
||||
if (!results.Any() && colors.TryGetValue(controllable.NetEntity, out var color))
|
||||
if (!results.Any() && _blipColors.TryGetValue(controllable.NetEntity, out var color))
|
||||
handle.DrawLine(Vector2.Transform(worldPos, worldToView), cursorViewPos, color.WithAlpha(0.3f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,16 @@ public sealed partial class RadarBlipsSystem : EntitySystem
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
|
||||
private const double BlipStaleSeconds = 3.0;
|
||||
private static readonly List<(Vector2 Start, Vector2 End, float Thickness, Color Color)> EmptyHitscanList = new();
|
||||
private TimeSpan _lastRequestTime = TimeSpan.Zero;
|
||||
private static readonly TimeSpan RequestThrottle = TimeSpan.FromMilliseconds(250);
|
||||
private static readonly TimeSpan RequestThrottle = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private TimeSpan _lastUpdatedTime;
|
||||
private List<BlipNetData> _blips = new();
|
||||
private List<(Vector2 Start, Vector2 End, float Thickness, Color Color)> _hitscans = new();
|
||||
private List<HitscanNetData> _hitscans = new();
|
||||
private List<BlipConfig> _configPalette = new();
|
||||
|
||||
// cached results to avoid allocating on every draw/frame
|
||||
private readonly List<BlipData> _cachedBlipData = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -30,24 +33,9 @@ public sealed partial class RadarBlipsSystem : EntitySystem
|
||||
|
||||
private void HandleReceiveBlips(GiveBlipsEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
if (ev?.Blips == null)
|
||||
{
|
||||
_blips.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_blips = ev.Blips;
|
||||
}
|
||||
|
||||
if (ev?.HitscanLines == null)
|
||||
{
|
||||
_hitscans = EmptyHitscanList;
|
||||
}
|
||||
else
|
||||
{
|
||||
_hitscans = ev.HitscanLines;
|
||||
}
|
||||
|
||||
_configPalette = ev.ConfigPalette;
|
||||
_blips = ev.Blips;
|
||||
_hitscans = ev.HitscanLines;
|
||||
_lastUpdatedTime = _timing.CurTime;
|
||||
}
|
||||
|
||||
@@ -79,13 +67,12 @@ public sealed partial class RadarBlipsSystem : EntitySystem
|
||||
/// </summary>
|
||||
public List<BlipData> GetCurrentBlips()
|
||||
{
|
||||
// If it's been more than the stale threshold since our last update,
|
||||
// the data is considered stale - return an empty list
|
||||
// clear the cache and bail early if the data is stale
|
||||
_cachedBlipData.Clear();
|
||||
if (_timing.CurTime.TotalSeconds - _lastUpdatedTime.TotalSeconds > BlipStaleSeconds)
|
||||
return new();
|
||||
|
||||
var result = new List<BlipData>(_blips.Count);
|
||||
return _cachedBlipData;
|
||||
|
||||
// populate the cached list instead of allocating a new one each frame
|
||||
foreach (var blip in _blips)
|
||||
{
|
||||
var coord = GetCoordinates(blip.Position);
|
||||
@@ -97,37 +84,32 @@ public sealed partial class RadarBlipsSystem : EntitySystem
|
||||
|
||||
var predictedMap = _xform.ToMapCoordinates(predictedPos);
|
||||
|
||||
var config = blip.Config;
|
||||
var config = _configPalette[blip.ConfigIndex];
|
||||
var rotation = blip.Rotation;
|
||||
// hijack our shape if we're on a grid and we want to do that
|
||||
if (_map.TryFindGridAt(predictedMap, out var grid, out _) && grid != EntityUid.Invalid && blip.OnGridConfig != null)
|
||||
config = blip.OnGridConfig.Value;
|
||||
if (_map.TryFindGridAt(predictedMap, out var grid, out _) && grid != EntityUid.Invalid)
|
||||
{
|
||||
if (blip.OnGridConfigIndex is { } gridIdx)
|
||||
config = _configPalette[gridIdx];
|
||||
rotation += Transform(grid).LocalRotation;
|
||||
}
|
||||
var maybeGrid = grid != EntityUid.Invalid ? grid : (EntityUid?)null;
|
||||
|
||||
result.Add(new(blip.Uid, predictedPos, blip.Rotation, maybeGrid, config));
|
||||
_cachedBlipData.Add(new(blip.Uid, predictedPos, rotation, maybeGrid, config));
|
||||
}
|
||||
|
||||
return result;
|
||||
return _cachedBlipData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hitscan lines to be rendered on the radar
|
||||
/// </summary>
|
||||
public List<(Vector2 Start, Vector2 End, float Thickness, Color Color)> GetHitscanLines()
|
||||
public List<HitscanNetData> GetHitscanLines()
|
||||
{
|
||||
if (_timing.CurTime.TotalSeconds - _lastUpdatedTime.TotalSeconds > BlipStaleSeconds)
|
||||
return new List<(Vector2, Vector2, float, Color)>();
|
||||
return new();
|
||||
|
||||
var result = new List<(Vector2, Vector2, float, Color)>(_hitscans.Count);
|
||||
|
||||
foreach (var hitscan in _hitscans)
|
||||
{
|
||||
var worldStart = hitscan.Start;
|
||||
var worldEnd = hitscan.End;
|
||||
|
||||
result.Add((worldStart, worldEnd, hitscan.Thickness, hitscan.Color));
|
||||
}
|
||||
|
||||
return result;
|
||||
return _hitscans;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,9 @@ public sealed partial class StationPickerControl : PickerControl
|
||||
|
||||
foreach (var (stationEntity, stationJobInformation) in stationList)
|
||||
{
|
||||
var icon = stationJobInformation.StationDisplayInfo?.StationIcon;
|
||||
var iconTexture = icon != null ? _spriteSystem.Frame0(icon) : null;
|
||||
|
||||
var viewState = new StationListItem.ViewState(
|
||||
stationEntity,
|
||||
stationJobInformation.GetStationNameWithJobCount(),
|
||||
@@ -136,7 +139,7 @@ public sealed partial class StationPickerControl : PickerControl
|
||||
? _loc.GetString(stationJobInformation.StationDisplayInfo.StationDescription)
|
||||
: "",
|
||||
_lastSelectedStation?.StationEntity == stationEntity,
|
||||
stationJobInformation.StationDisplayInfo?.StationIcon?.CanonPath
|
||||
iconTexture
|
||||
);
|
||||
|
||||
// Always select the first station in the list if none is selected yet.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
@@ -13,7 +14,7 @@ public sealed partial class StationListItem : PanelContainer
|
||||
string stationSubtext,
|
||||
string stationDescription,
|
||||
bool selected,
|
||||
string? iconPath)
|
||||
Texture? icon)
|
||||
{
|
||||
public string StationName { get; } = stationName;
|
||||
public string StationSubtext { get; } = stationSubtext;
|
||||
@@ -22,7 +23,7 @@ public sealed partial class StationListItem : PanelContainer
|
||||
|
||||
public bool Selected { get; set; } = selected;
|
||||
|
||||
public string IconPath { get; } = iconPath ?? "";
|
||||
public Texture? Icon { get; } = icon;
|
||||
}
|
||||
|
||||
public StationListItem(ViewState state)
|
||||
@@ -35,9 +36,9 @@ public sealed partial class StationListItem : PanelContainer
|
||||
// Disallow repeated selection of same station that does UI reloads.
|
||||
StationButton.Disabled = state.Selected;
|
||||
|
||||
if (state.IconPath.Length > 0)
|
||||
if (state.Icon != null)
|
||||
{
|
||||
StationIcon.TexturePath = state.IconPath;
|
||||
StationIcon.Texture = state.Icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class RMCCombatModeUISystem : EntitySystem
|
||||
_hands.GetActiveHandEntity() is { } held &&
|
||||
_rmcCombatMode.GetCrosshair(held) != null)
|
||||
{
|
||||
_crosshairCursor ??= _clyde.CreateCursor(new Image<Rgba32>(1, 1), Vector2i.One);
|
||||
_crosshairCursor ??= _clyde.CreateCursor(new Image<Rgba32>(1, 1), Vector2i.Zero);
|
||||
_clyde.SetCursor(_crosshairCursor);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<OutputPath>..\bin\Content.IntegrationTests\</OutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
@@ -17,12 +15,12 @@
|
||||
<ProjectReference Include="..\Content.Server\Content.Server.csproj" />
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.UnitTesting\Robust.UnitTesting.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Server.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Testing.props" />
|
||||
</Project>
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
global using NUnit.Framework;
|
||||
global using System;
|
||||
global using System.Threading.Tasks;
|
||||
global using Robust.UnitTesting.Pool;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
/// <summary>
|
||||
/// Simple data class that stored information about a map being used by a test.
|
||||
/// </summary>
|
||||
public sealed class TestMapData
|
||||
{
|
||||
public EntityUid MapUid { get; set; }
|
||||
public Entity<MapGridComponent> Grid;
|
||||
public MapId MapId;
|
||||
public EntityCoordinates GridCoords { get; set; }
|
||||
public MapCoordinates MapCoords { get; set; }
|
||||
public TileRef Tile { get; set; }
|
||||
|
||||
// Client-side uids
|
||||
public EntityUid CMapUid { get; set; }
|
||||
public EntityUid CGridUid { get; set; }
|
||||
public EntityCoordinates CGridCoords { get; set; }
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
private readonly Dictionary<string, object> _modifiedClientCvars = new();
|
||||
private readonly Dictionary<string, object> _modifiedServerCvars = new();
|
||||
|
||||
private void OnServerCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
_modifiedServerCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
private void OnClientCvarChanged(CVarChangeInfo args)
|
||||
{
|
||||
_modifiedClientCvars.TryAdd(args.Name, args.OldValue);
|
||||
}
|
||||
|
||||
internal void ClearModifiedCvars()
|
||||
{
|
||||
_modifiedClientCvars.Clear();
|
||||
_modifiedServerCvars.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts any cvars that were modified during a test back to their original values.
|
||||
/// </summary>
|
||||
public async Task RevertModifiedCvars()
|
||||
{
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
foreach (var (name, value) in _modifiedServerCvars)
|
||||
{
|
||||
if (Server.CfgMan.GetCVar(name).Equals(value))
|
||||
continue;
|
||||
Server.Log.Info($"Resetting cvar {name} to {value}");
|
||||
Server.CfgMan.SetCVar(name, value);
|
||||
}
|
||||
|
||||
// I just love order dependent cvars
|
||||
if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
|
||||
Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
|
||||
|
||||
});
|
||||
|
||||
await Client.WaitPost(() =>
|
||||
{
|
||||
foreach (var (name, value) in _modifiedClientCvars)
|
||||
{
|
||||
if (Client.CfgMan.GetCVar(name).Equals(value))
|
||||
continue;
|
||||
|
||||
var flags = Client.CfgMan.GetCVarFlags(name);
|
||||
if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
|
||||
continue;
|
||||
|
||||
Client.Log.Info($"Resetting cvar {name} to {value}");
|
||||
Client.CfgMan.SetCVar(name, value);
|
||||
}
|
||||
});
|
||||
|
||||
ClearModifiedCvars();
|
||||
}
|
||||
}
|
||||
@@ -1,180 +1,68 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.UnitTesting;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// Contains misc helper functions to make writing tests easier.
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
public Task<TestMapData> CreateTestMap(bool initialized = true)
|
||||
=> CreateTestMap(initialized, "Plating");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a map, a grid, and a tile, and gives back references to them.
|
||||
/// Loads a test map and returns a <see cref="TestMapData"/> representing it.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(TestMap))]
|
||||
public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
|
||||
/// <param name="testMapPath">The <see cref="ResPath"/> to the test map to load.</param>
|
||||
/// <param name="initialized">Whether to initialize the map on load.</param>
|
||||
/// <returns>A <see cref="TestMapData"/> representing the loaded map.</returns>
|
||||
public async Task<TestMapData> LoadTestMap(ResPath testMapPath, bool initialized = true)
|
||||
{
|
||||
var mapData = new TestMapData();
|
||||
TestMap = mapData;
|
||||
await Server.WaitIdleAsync();
|
||||
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
|
||||
TestMapData mapData = new();
|
||||
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = initialized };
|
||||
var mapLoaderSys = Server.EntMan.System<MapLoaderSystem>();
|
||||
var mapSys = Server.System<SharedMapSystem>();
|
||||
|
||||
TestMap = mapData;
|
||||
await Server.WaitPost(() =>
|
||||
// Load our test map in and assert that it exists.
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
mapData.MapUid = Server.System<SharedMapSystem>().CreateMap(out mapData.MapId, runMapInit: initialized);
|
||||
mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
|
||||
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
|
||||
var plating = tileDefinitionManager[tile];
|
||||
var platingTile = new Tile(plating.TileId);
|
||||
Server.System<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
|
||||
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
|
||||
mapData.Tile = Server.System<SharedMapSystem>().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
|
||||
});
|
||||
Assert.That(mapLoaderSys.TryLoadMap(testMapPath, out var map, out var gridSet, deserializationOptions),
|
||||
$"Failed to load map {testMapPath}.");
|
||||
Assert.That(gridSet, Is.Not.Empty, "There were no grids loaded from the map!");
|
||||
|
||||
TestMap = mapData;
|
||||
if (!Settings.Connected)
|
||||
return mapData;
|
||||
mapData.MapUid = map!.Value.Owner;
|
||||
mapData.MapId = map!.Value.Comp.MapId;
|
||||
mapData.Grid = gridSet!.First();
|
||||
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
|
||||
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
|
||||
mapData.Tile = mapSys.GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
|
||||
});
|
||||
|
||||
await RunTicksSync(10);
|
||||
mapData.CMapUid = ToClientUid(mapData.MapUid);
|
||||
mapData.CGridUid = ToClientUid(mapData.Grid);
|
||||
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
|
||||
|
||||
TestMap = mapData;
|
||||
return mapData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a client-side uid into a server-side uid
|
||||
/// </summary>
|
||||
public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a server-side uid into a client-side uid
|
||||
/// </summary>
|
||||
public EntityUid ToClientUid(EntityUid uid) => ConvertUid(uid, Server, Client);
|
||||
|
||||
private static EntityUid ConvertUid(
|
||||
EntityUid uid,
|
||||
RobustIntegrationTest.IntegrationInstance source,
|
||||
RobustIntegrationTest.IntegrationInstance destination)
|
||||
{
|
||||
if (!uid.IsValid())
|
||||
return EntityUid.Invalid;
|
||||
|
||||
if (!source.EntMan.TryGetComponent<MetaDataComponent>(uid, out var meta))
|
||||
{
|
||||
Assert.Fail($"Failed to resolve MetaData while converting the EntityUid for entity {uid}");
|
||||
return EntityUid.Invalid;
|
||||
}
|
||||
|
||||
if (!destination.EntMan.TryGetEntity(meta.NetEntity, out var otherUid))
|
||||
{
|
||||
Assert.Fail($"Failed to resolve net ID while converting the EntityUid entity {source.EntMan.ToPrettyString(uid)}");
|
||||
return EntityUid.Invalid;
|
||||
}
|
||||
|
||||
return otherUid.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command on the server and wait some number of ticks.
|
||||
/// </summary>
|
||||
public async Task WaitCommand(string cmd, int numTicks = 10)
|
||||
{
|
||||
await Server.ExecuteCommand(cmd);
|
||||
await RunTicksSync(numTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command on the client and wait some number of ticks.
|
||||
/// </summary>
|
||||
public async Task WaitClientCommand(string cmd, int numTicks = 10)
|
||||
{
|
||||
await Client.ExecuteCommand(cmd);
|
||||
await RunTicksSync(numTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
where T : IComponent, new()
|
||||
{
|
||||
if (!Server.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out var reg)
|
||||
&& !Client.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out reg))
|
||||
{
|
||||
Assert.Fail($"Unknown component: {typeof(T).Name}");
|
||||
return new();
|
||||
}
|
||||
|
||||
var id = reg.Name;
|
||||
var list = new List<(EntityPrototype, T)>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (ignored != null && ignored.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
if (ignoreAbstract && proto.Abstract)
|
||||
continue;
|
||||
|
||||
if (ignoreTestPrototypes && IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (proto.Components.TryGetComponent(id, out var cmp))
|
||||
list.Add((proto, (T)cmp));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
{
|
||||
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
|
||||
var list = new List<EntityPrototype>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (ignored != null && ignored.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
if (ignoreAbstract && proto.Abstract)
|
||||
continue;
|
||||
|
||||
if (ignoreTestPrototypes && IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (proto.Components.ContainsKey(id))
|
||||
list.Add((proto));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
|
||||
/// </summary>
|
||||
public async Task SetAntagPreference(ProtoId<AntagPrototype> id, bool value, NetUserId? user = null)
|
||||
{
|
||||
user ??= Client.User!.Value;
|
||||
if (user is not {} userId)
|
||||
if (user is not { } userId)
|
||||
return;
|
||||
|
||||
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
|
||||
@@ -183,7 +71,7 @@ public sealed partial class TestPair
|
||||
// Automatic preference resetting only resets slot 0.
|
||||
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
|
||||
|
||||
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
|
||||
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
|
||||
var newProfile = profile.WithAntagPreference(id, value);
|
||||
_modifiedProfiles.Add(userId);
|
||||
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
|
||||
@@ -211,7 +99,7 @@ public sealed partial class TestPair
|
||||
|
||||
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
|
||||
var prefs = prefMan.GetPreferences(user);
|
||||
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
|
||||
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
|
||||
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
|
||||
|
||||
// Automatic preference resetting only resets slot 0.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// This partial class contains helper methods to deal with yaml prototypes.
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
private Dictionary<Type, HashSet<string>> _loadedPrototypes = new();
|
||||
private HashSet<string> _loadedEntityPrototypes = new();
|
||||
|
||||
public async Task LoadPrototypes(List<string> prototypes)
|
||||
{
|
||||
await LoadPrototypes(Server, prototypes);
|
||||
await LoadPrototypes(Client, prototypes);
|
||||
}
|
||||
|
||||
private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List<string> prototypes)
|
||||
{
|
||||
var changed = new Dictionary<Type, HashSet<string>>();
|
||||
foreach (var file in prototypes)
|
||||
{
|
||||
instance.ProtoMan.LoadString(file, changed: changed);
|
||||
}
|
||||
|
||||
await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
|
||||
|
||||
foreach (var (kind, ids) in changed)
|
||||
{
|
||||
_loadedPrototypes.GetOrNew(kind).UnionWith(ids);
|
||||
}
|
||||
|
||||
if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
|
||||
_loadedEntityPrototypes.UnionWith(entIds);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype(EntityPrototype proto)
|
||||
{
|
||||
return _loadedEntityPrototypes.Contains(proto.ID);
|
||||
}
|
||||
|
||||
public bool IsTestEntityPrototype(string id)
|
||||
{
|
||||
return _loadedEntityPrototypes.Contains(id);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype<TPrototype>(string id) where TPrototype : IPrototype
|
||||
{
|
||||
return IsTestPrototype(typeof(TPrototype), id);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype<TPrototype>(TPrototype proto) where TPrototype : IPrototype
|
||||
{
|
||||
return IsTestPrototype(typeof(TPrototype), proto.ID);
|
||||
}
|
||||
|
||||
public bool IsTestPrototype(Type kind, string id)
|
||||
{
|
||||
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
|
||||
}
|
||||
}
|
||||
@@ -8,82 +8,17 @@ using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// This partial class contains logic related to recycling & disposing test pairs.
|
||||
public sealed partial class TestPair : IAsyncDisposable
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
public PairState State { get; private set; } = PairState.Ready;
|
||||
|
||||
private async Task OnDirtyDispose()
|
||||
protected override async Task Cleanup()
|
||||
{
|
||||
var usageTime = Watch.Elapsed;
|
||||
Watch.Restart();
|
||||
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
|
||||
Kill();
|
||||
var disposeTime = Watch.Elapsed;
|
||||
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
|
||||
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
|
||||
// because someone forgot to clean-return the pair.
|
||||
Assert.Warn("Test was dirty-disposed.");
|
||||
}
|
||||
|
||||
private async Task OnCleanDispose()
|
||||
{
|
||||
await Server.WaitIdleAsync();
|
||||
await Client.WaitIdleAsync();
|
||||
await base.Cleanup();
|
||||
await ResetModifiedPreferences();
|
||||
await Server.RemoveAllDummySessions();
|
||||
|
||||
if (TestMap != null)
|
||||
{
|
||||
await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
|
||||
TestMap = null;
|
||||
}
|
||||
|
||||
await RevertModifiedCvars();
|
||||
|
||||
var usageTime = Watch.Elapsed;
|
||||
Watch.Restart();
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
|
||||
// Let any last minute failures the test cause happen.
|
||||
await ReallyBeIdle();
|
||||
if (!Settings.Destructive)
|
||||
{
|
||||
if (Client.IsAlive == false)
|
||||
{
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
|
||||
}
|
||||
|
||||
if (Server.IsAlive == false)
|
||||
{
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
|
||||
}
|
||||
}
|
||||
|
||||
if (Settings.MustNotBeReused)
|
||||
{
|
||||
Kill();
|
||||
await ReallyBeIdle();
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
|
||||
return;
|
||||
}
|
||||
|
||||
var sRuntimeLog = Server.ResolveDependency<IRuntimeLog>();
|
||||
if (sRuntimeLog.ExceptionCount > 0)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
|
||||
var cRuntimeLog = Client.ResolveDependency<IRuntimeLog>();
|
||||
if (cRuntimeLog.ExceptionCount > 0)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
|
||||
|
||||
var returnTime = Watch.Elapsed;
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
|
||||
}
|
||||
|
||||
private async Task ResetModifiedPreferences()
|
||||
@@ -93,61 +28,14 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
{
|
||||
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
|
||||
}
|
||||
|
||||
_modifiedProfiles.Clear();
|
||||
}
|
||||
|
||||
public async ValueTask CleanReturnAsync()
|
||||
protected override async Task Recycle(PairSettings next, TextWriter testOut)
|
||||
{
|
||||
if (State != PairState.InUse)
|
||||
throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||
|
||||
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
|
||||
State = PairState.CleanDisposed;
|
||||
await OnCleanDispose();
|
||||
State = PairState.Ready;
|
||||
PoolManager.NoCheckReturn(this);
|
||||
ClearContext();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case PairState.Dead:
|
||||
case PairState.Ready:
|
||||
break;
|
||||
case PairState.InUse:
|
||||
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
|
||||
await OnDirtyDispose();
|
||||
PoolManager.NoCheckReturn(this);
|
||||
ClearContext();
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
|
||||
{
|
||||
Settings = default!;
|
||||
Watch.Restart();
|
||||
await testOut.WriteLineAsync($"Recycling...");
|
||||
|
||||
var gameTicker = Server.System<GameTicker>();
|
||||
var cNetMgr = Client.ResolveDependency<IClientNetManager>();
|
||||
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Disconnect the client if they are connected.
|
||||
if (cNetMgr.IsConnected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
|
||||
await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
|
||||
await RunTicksSync(1);
|
||||
}
|
||||
Assert.That(cNetMgr.IsConnected, Is.False);
|
||||
|
||||
// Move to pre-round lobby. Required to toggle dummy ticker on and off
|
||||
var gameTicker = Server.System<GameTicker>();
|
||||
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
|
||||
@@ -160,8 +48,7 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
|
||||
//Apply Cvars
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
|
||||
await PoolManager.SetupCVars(Client, settings);
|
||||
await PoolManager.SetupCVars(Server, settings);
|
||||
await ApplySettings(next);
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Restart server.
|
||||
@@ -169,52 +56,30 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
await Server.WaitPost(() => Server.EntMan.FlushEntities());
|
||||
await Server.WaitPost(() => gameTicker.RestartRound());
|
||||
await RunTicksSync(1);
|
||||
|
||||
// Connect client
|
||||
if (settings.ShouldBeConnected)
|
||||
{
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
|
||||
Client.SetConnectTarget(Server);
|
||||
await Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
|
||||
}
|
||||
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
|
||||
await ReallyBeIdle();
|
||||
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
|
||||
}
|
||||
|
||||
public void ValidateSettings(PoolSettings settings)
|
||||
public override void ValidateSettings(PairSettings s)
|
||||
{
|
||||
base.ValidateSettings(s);
|
||||
var settings = (PoolSettings) s;
|
||||
|
||||
var cfg = Server.CfgMan;
|
||||
Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
|
||||
Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
|
||||
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
|
||||
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker));
|
||||
|
||||
var entMan = Server.ResolveDependency<EntityManager>();
|
||||
var ticker = entMan.System<GameTicker>();
|
||||
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
|
||||
var ticker = Server.System<GameTicker>();
|
||||
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker));
|
||||
|
||||
var expectPreRound = settings.InLobby | settings.DummyTicker;
|
||||
var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
|
||||
Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
|
||||
|
||||
var baseClient = Client.ResolveDependency<IBaseClient>();
|
||||
var netMan = Client.ResolveDependency<INetManager>();
|
||||
Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
|
||||
|
||||
if (!settings.ShouldBeConnected)
|
||||
if (ticker.DummyTicker || !settings.Connected)
|
||||
return;
|
||||
|
||||
Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
|
||||
var cPlayer = Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
|
||||
var sPlayer = Server.ResolveDependency<IPlayerManager>();
|
||||
Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
|
||||
var sPlayer = Server.ResolveDependency<ISharedPlayerManager>();
|
||||
var session = sPlayer.Sessions.Single();
|
||||
Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
|
||||
|
||||
if (ticker.DummyTicker)
|
||||
return;
|
||||
|
||||
var status = ticker.PlayerGameStatuses[session.UserId];
|
||||
var expected = settings.InLobby
|
||||
? PlayerGameStatus.NotReadyToPlay
|
||||
@@ -229,11 +94,11 @@ public sealed partial class TestPair : IAsyncDisposable
|
||||
}
|
||||
|
||||
Assert.That(session.AttachedEntity, Is.Not.Null);
|
||||
Assert.That(entMan.EntityExists(session.AttachedEntity));
|
||||
Assert.That(entMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
|
||||
var mindCont = entMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
|
||||
Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
|
||||
Assert.That(Server.EntMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
|
||||
var mindCont = Server.EntMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
|
||||
Assert.That(mindCont.Mind, Is.Not.Null);
|
||||
Assert.That(entMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
|
||||
Assert.That(Server.EntMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
|
||||
Assert.That(mind!.VisitingEntity, Is.Null);
|
||||
Assert.That(mind.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
|
||||
Assert.That(mind.UserId, Is.EqualTo(session.UserId));
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
|
||||
// This partial class contains methods for running the server/client pairs for some number of ticks
|
||||
public sealed partial class TestPair
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the server-client pair in sync
|
||||
/// </summary>
|
||||
/// <param name="ticks">How many ticks to run them for</param>
|
||||
public async Task RunTicksSync(int ticks)
|
||||
{
|
||||
for (var i = 0; i < ticks; i++)
|
||||
{
|
||||
await Server.WaitRunTicks(1);
|
||||
await Client.WaitRunTicks(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a time interval to some number of ticks.
|
||||
/// </summary>
|
||||
public int SecondsToTicks(float seconds)
|
||||
{
|
||||
return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the server & client in sync for some amount of time
|
||||
/// </summary>
|
||||
public async Task RunSeconds(float seconds)
|
||||
{
|
||||
await RunTicksSync(SecondsToTicks(seconds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
|
||||
/// </summary>
|
||||
/// <param name="runTicks">How many ticks to run</param>
|
||||
public async Task ReallyBeIdle(int runTicks = 25)
|
||||
{
|
||||
for (var i = 0; i < runTicks; i++)
|
||||
{
|
||||
await Client.WaitRunTicks(1);
|
||||
await Server.WaitRunTicks(1);
|
||||
for (var idleCycles = 0; idleCycles < 4; idleCycles++)
|
||||
{
|
||||
await Client.WaitIdleAsync();
|
||||
await Server.WaitIdleAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the server/clients until the ticks are synchronized.
|
||||
/// By default the client will be one tick ahead of the server.
|
||||
/// </summary>
|
||||
public async Task SyncTicks(int targetDelta = 1)
|
||||
{
|
||||
var sTick = (int)Server.Timing.CurTick.Value;
|
||||
var cTick = (int)Client.Timing.CurTick.Value;
|
||||
var delta = cTick - sTick;
|
||||
|
||||
if (delta == targetDelta)
|
||||
return;
|
||||
if (delta > targetDelta)
|
||||
await Server.WaitRunTicks(delta - targetDelta);
|
||||
else
|
||||
await Client.WaitRunTicks(targetDelta - delta);
|
||||
|
||||
sTick = (int)Server.Timing.CurTick.Value;
|
||||
cTick = (int)Client.Timing.CurTick.Value;
|
||||
delta = cTick - sTick;
|
||||
Assert.That(delta, Is.EqualTo(targetDelta));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Client.IoC;
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Content.IntegrationTests.Tests.Destructible;
|
||||
using Content.IntegrationTests.Tests.DeviceNetwork;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests.Pair;
|
||||
@@ -18,156 +19,109 @@ namespace Content.IntegrationTests.Pair;
|
||||
/// <summary>
|
||||
/// This object wraps a pooled server+client pair.
|
||||
/// </summary>
|
||||
public sealed partial class TestPair
|
||||
public sealed partial class TestPair : RobustIntegrationTest.TestPair
|
||||
{
|
||||
public readonly int Id;
|
||||
private bool _initialized;
|
||||
private TextWriter _testOut = default!;
|
||||
public readonly Stopwatch Watch = new();
|
||||
public readonly List<string> TestHistory = new();
|
||||
public PoolSettings Settings = default!;
|
||||
public TestMapData? TestMap;
|
||||
private List<NetUserId> _modifiedProfiles = new();
|
||||
|
||||
private int _nextServerSeed;
|
||||
private int _nextClientSeed;
|
||||
|
||||
public int ServerSeed;
|
||||
public int ClientSeed;
|
||||
|
||||
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
|
||||
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
|
||||
|
||||
public void Deconstruct(
|
||||
out RobustIntegrationTest.ServerIntegrationInstance server,
|
||||
out RobustIntegrationTest.ClientIntegrationInstance client)
|
||||
{
|
||||
server = Server;
|
||||
client = Client;
|
||||
}
|
||||
|
||||
public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
|
||||
|
||||
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
|
||||
|
||||
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
|
||||
public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
|
||||
|
||||
public TestPair(int id)
|
||||
protected override async Task Initialize()
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
await base.Initialize();
|
||||
|
||||
public async Task Initialize(PoolSettings settings, TextWriter testOut, List<string> testPrototypes)
|
||||
{
|
||||
if (_initialized)
|
||||
throw new InvalidOperationException("Already initialized");
|
||||
|
||||
_initialized = true;
|
||||
Settings = settings;
|
||||
(Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
|
||||
(Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
|
||||
ActivateContext(testOut);
|
||||
|
||||
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
|
||||
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
|
||||
|
||||
if (!settings.NoLoadTestPrototypes)
|
||||
await LoadPrototypes(testPrototypes!);
|
||||
|
||||
if (!settings.UseDummyTicker)
|
||||
// Prevent info log spam in some tests (particularly SpawnAndDeleteAllEntitiesOnDifferentMaps)
|
||||
Server.System<SharedMapSystem>().Log.Level = LogLevel.Warning;
|
||||
Client.EntMan.EntitySysManager.SystemLoaded += (_, e) =>
|
||||
{
|
||||
var gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
|
||||
if (e.System is SharedMapSystem map)
|
||||
map.Log.Level = LogLevel.Warning;
|
||||
};
|
||||
|
||||
var settings = (PoolSettings)Settings;
|
||||
if (!settings.DummyTicker)
|
||||
{
|
||||
var gameTicker = Server.System<GameTicker>();
|
||||
await Server.WaitPost(() => gameTicker.RestartRound());
|
||||
}
|
||||
}
|
||||
|
||||
// Always initially connect clients to generate an initial random set of preferences/profiles.
|
||||
// This is to try and prevent issues where if the first test that connects the client is consistently some test
|
||||
// that uses a fixed seed, it would effectively prevent it from beingrandomized.
|
||||
public override async Task RevertModifiedCvars()
|
||||
{
|
||||
// I just love order dependent cvars
|
||||
// I.e., cvars that when changed automatically cause others to also change.
|
||||
var modified = ModifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik);
|
||||
|
||||
Client.SetConnectTarget(Server);
|
||||
await Client.WaitIdleAsync();
|
||||
var netMgr = Client.ResolveDependency<IClientNetManager>();
|
||||
await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
|
||||
await ReallyBeIdle(10);
|
||||
await Client.WaitRunTicks(1);
|
||||
await base.RevertModifiedCvars();
|
||||
|
||||
if (!settings.ShouldBeConnected)
|
||||
if (!modified)
|
||||
return;
|
||||
|
||||
await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
|
||||
ClearModifiedCvars();
|
||||
}
|
||||
|
||||
protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
|
||||
{
|
||||
var next = (PoolSettings)n;
|
||||
await base.ApplySettings(instance, next);
|
||||
var cfg = instance.CfgMan;
|
||||
await instance.WaitPost(() =>
|
||||
{
|
||||
await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
|
||||
await ReallyBeIdle(10);
|
||||
}
|
||||
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
|
||||
cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
_nextClientSeed = cRand.Next();
|
||||
_nextServerSeed = sRand.Next();
|
||||
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
|
||||
cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
|
||||
cfg.SetCVar(CCVars.GameMap, next.Map);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
|
||||
cfg.SetCVar(CCVars.AdminLogsEnabled, next.AdminLogsEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
|
||||
{
|
||||
State = PairState.Dead;
|
||||
ServerLogHandler.ShuttingDown = true;
|
||||
ClientLogHandler.ShuttingDown = true;
|
||||
Server.Dispose();
|
||||
Client.Dispose();
|
||||
}
|
||||
var opts = base.ClientOptions();
|
||||
|
||||
private void ClearContext()
|
||||
{
|
||||
_testOut = default!;
|
||||
ServerLogHandler.ClearContext();
|
||||
ClientLogHandler.ClearContext();
|
||||
}
|
||||
|
||||
public void ActivateContext(TextWriter testOut)
|
||||
{
|
||||
_testOut = testOut;
|
||||
ServerLogHandler.ActivateContext(testOut);
|
||||
ClientLogHandler.ActivateContext(testOut);
|
||||
}
|
||||
|
||||
public void Use()
|
||||
{
|
||||
if (State != PairState.Ready)
|
||||
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
|
||||
State = PairState.InUse;
|
||||
}
|
||||
|
||||
public enum PairState : byte
|
||||
{
|
||||
Ready = 0,
|
||||
InUse = 1,
|
||||
CleanDisposed = 2,
|
||||
Dead = 3,
|
||||
}
|
||||
|
||||
public void SetupSeed()
|
||||
{
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ServerSeed is { } severSeed)
|
||||
opts.LoadTestAssembly = false;
|
||||
opts.ContentStart = true;
|
||||
opts.FailureLogLevel = LogLevel.Warning;
|
||||
opts.Options = new()
|
||||
{
|
||||
ServerSeed = severSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerSeed = _nextServerSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
_nextServerSeed = sRand.Next();
|
||||
}
|
||||
LoadConfigAndUserData = false,
|
||||
};
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ClientSeed is { } clientSeed)
|
||||
opts.BeforeStart += () =>
|
||||
{
|
||||
ClientSeed = clientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
}
|
||||
else
|
||||
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
|
||||
{
|
||||
ClientBeforeIoC = () => IoCManager.Register<IParallaxManager, DummyParallaxManager>(true)
|
||||
});
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
|
||||
protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
|
||||
{
|
||||
var opts = base.ServerOptions();
|
||||
|
||||
opts.LoadTestAssembly = false;
|
||||
opts.ContentStart = true;
|
||||
opts.Options = new()
|
||||
{
|
||||
ClientSeed = _nextClientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
_nextClientSeed = cRand.Next();
|
||||
}
|
||||
LoadConfigAndUserData = false,
|
||||
};
|
||||
|
||||
opts.BeforeStart += () =>
|
||||
{
|
||||
// Server-only systems (i.e., systems that subscribe to events with server-only components)
|
||||
// There's probably a better way to do this.
|
||||
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
|
||||
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#nullable enable
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
// Partial class containing cvar logic
|
||||
// Partial class containing test cvars
|
||||
// This could probably be merged into the main file, but I'm keeping it separate to reduce
|
||||
// conflicts for forks.
|
||||
public static partial class PoolManager
|
||||
{
|
||||
private static readonly (string cvar, string value)[] TestCvars =
|
||||
public static readonly (string cvar, string value)[] TestCvars =
|
||||
{
|
||||
// @formatter:off
|
||||
(CCVars.DatabaseSynchronous.Name, "true"),
|
||||
@@ -17,9 +16,7 @@ public static partial class PoolManager
|
||||
(CCVars.HolidaysEnabled.Name, "false"),
|
||||
(CCVars.GameMap.Name, TestMap),
|
||||
(CCVars.AdminLogsQueueSendDelay.Name, "0"),
|
||||
(CVars.NetPVS.Name, "false"),
|
||||
(CCVars.NPCMaxUpdates.Name, "999999"),
|
||||
(CVars.ThreadParallelCount.Name, "1"),
|
||||
(CCVars.GameRoleTimers.Name, "false"),
|
||||
(CCVars.GameRoleWhitelist.Name, "false"),
|
||||
(CCVars.GridFill.Name, "false"),
|
||||
@@ -29,55 +26,16 @@ public static partial class PoolManager
|
||||
(CCVars.ProcgenPreload.Name, "false"),
|
||||
(CCVars.WorldgenEnabled.Name, "false"),
|
||||
(CCVars.GatewayGeneratorEnabled.Name, "false"),
|
||||
(CVars.ReplayClientRecordingEnabled.Name, "false"),
|
||||
(CVars.ReplayServerRecordingEnabled.Name, "false"),
|
||||
(CCVars.GameDummyTicker.Name, "true"),
|
||||
(CCVars.GameLobbyEnabled.Name, "false"),
|
||||
(CCVars.ConfigPresetDevelopment.Name, "false"),
|
||||
(CCVars.AdminLogsEnabled.Name, "false"),
|
||||
(CCVars.AutosaveEnabled.Name, "false"),
|
||||
(CVars.NetBufferSize.Name, "0"),
|
||||
(CCVars.InteractionRateLimitCount.Name, "9999999"),
|
||||
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
|
||||
(CCVars.MovementMobPushing.Name, "false"),
|
||||
(CCVars.GameLobbyDefaultPreset.Name, "secret"), // Frontier: Adventure takes ages, default to secret
|
||||
(CCVars.StaticStorageUI.Name, "true"),// Frontier: causes storage test failures
|
||||
(CCVars.StorageLimit.Name, "1")// Frontier: test failures with multiple storage enabled
|
||||
(CCVars.StorageLimit.Name, "1")// Frontier: test failures with multiple storage
|
||||
};
|
||||
|
||||
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
|
||||
{
|
||||
var cfg = instance.ResolveDependency<IConfigurationManager>();
|
||||
await instance.WaitPost(() =>
|
||||
{
|
||||
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
|
||||
cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
|
||||
cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
|
||||
|
||||
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
|
||||
cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
|
||||
cfg.SetCVar(CCVars.GameMap, settings.Map);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
|
||||
cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
|
||||
|
||||
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
|
||||
cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
|
||||
|
||||
if (cfg.IsCVarRegistered(CCVars.GameLobbyDefaultPreset.Name) && !string.IsNullOrEmpty(settings.GameLobbyDefaultPreset)) // Frontier
|
||||
cfg.SetCVar(CCVars.GameLobbyDefaultPreset, settings.GameLobbyDefaultPreset); // Frontier
|
||||
});
|
||||
}
|
||||
|
||||
private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
|
||||
{
|
||||
foreach (var (cvar, value) in TestCvars)
|
||||
{
|
||||
options.CVarOverrides[cvar] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,368 +1,17 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Content.Client.IoC;
|
||||
using Content.Client.Parallax.Managers;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.IntegrationTests.Tests;
|
||||
using Content.IntegrationTests.Tests.Destructible;
|
||||
using Content.IntegrationTests.Tests.DeviceNetwork;
|
||||
using Content.IntegrationTests.Tests.Interaction.Click;
|
||||
using Robust.Client;
|
||||
using Robust.Server;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
|
||||
/// </summary>
|
||||
// The static class exist to avoid breaking changes
|
||||
public static partial class PoolManager
|
||||
{
|
||||
public static readonly ContentPoolManager Instance = new();
|
||||
public const string TestMap = "Empty";
|
||||
private static int _pairId;
|
||||
private static readonly object PairLock = new();
|
||||
private static bool _initialized;
|
||||
|
||||
// Pair, IsBorrowed
|
||||
private static readonly Dictionary<TestPair, bool> Pairs = new();
|
||||
private static bool _dead;
|
||||
private static Exception? _poolFailureReason;
|
||||
|
||||
private static HashSet<Assembly> _contentAssemblies = default!;
|
||||
|
||||
public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
|
||||
PoolSettings poolSettings,
|
||||
TextWriter testOut)
|
||||
{
|
||||
var options = new RobustIntegrationTest.ServerIntegrationOptions
|
||||
{
|
||||
ContentStart = true,
|
||||
Options = new ServerOptions()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
LoadContentResources = !poolSettings.NoLoadContent,
|
||||
},
|
||||
ContentAssemblies = _contentAssemblies.ToArray()
|
||||
};
|
||||
|
||||
var logHandler = new PoolTestLogHandler("SERVER");
|
||||
logHandler.ActivateContext(testOut);
|
||||
options.OverrideLogHandler = () => logHandler;
|
||||
|
||||
options.BeforeStart += () =>
|
||||
{
|
||||
// Server-only systems (i.e., systems that subscribe to events with server-only components)
|
||||
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
|
||||
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
|
||||
|
||||
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
IoCManager.Resolve<IConfigurationManager>()
|
||||
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
|
||||
};
|
||||
|
||||
SetDefaultCVars(options);
|
||||
var server = new RobustIntegrationTest.ServerIntegrationInstance(options);
|
||||
await server.WaitIdleAsync();
|
||||
await SetupCVars(server, poolSettings);
|
||||
return (server, logHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This shuts down the pool, and disposes all the server/client pairs.
|
||||
/// This is a one time operation to be used when the testing program is exiting.
|
||||
/// </summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
List<TestPair> localPairs;
|
||||
lock (PairLock)
|
||||
{
|
||||
if (_dead)
|
||||
return;
|
||||
_dead = true;
|
||||
localPairs = Pairs.Keys.ToList();
|
||||
}
|
||||
|
||||
foreach (var pair in localPairs)
|
||||
{
|
||||
pair.Kill();
|
||||
}
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public static string DeathReport()
|
||||
{
|
||||
lock (PairLock)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var borrowed = Pairs[pair];
|
||||
builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
|
||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
||||
{
|
||||
builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
|
||||
PoolSettings poolSettings,
|
||||
TextWriter testOut)
|
||||
{
|
||||
var options = new RobustIntegrationTest.ClientIntegrationOptions
|
||||
{
|
||||
FailureLogLevel = LogLevel.Warning,
|
||||
ContentStart = true,
|
||||
ContentAssemblies = new[]
|
||||
{
|
||||
typeof(Shared.Entry.EntryPoint).Assembly,
|
||||
typeof(Client.Entry.EntryPoint).Assembly,
|
||||
typeof(PoolManager).Assembly,
|
||||
}
|
||||
};
|
||||
|
||||
if (poolSettings.NoLoadContent)
|
||||
{
|
||||
Assert.Warn("NoLoadContent does not work on the client, ignoring");
|
||||
}
|
||||
|
||||
options.Options = new GameControllerOptions()
|
||||
{
|
||||
LoadConfigAndUserData = false,
|
||||
// LoadContentResources = !poolSettings.NoLoadContent
|
||||
};
|
||||
|
||||
var logHandler = new PoolTestLogHandler("CLIENT");
|
||||
logHandler.ActivateContext(testOut);
|
||||
options.OverrideLogHandler = () => logHandler;
|
||||
|
||||
options.BeforeStart += () =>
|
||||
{
|
||||
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
|
||||
{
|
||||
ClientBeforeIoC = () =>
|
||||
{
|
||||
// do not register extra systems or components here -- they will get cleared when the client is
|
||||
// disconnected. just use reflection.
|
||||
IoCManager.Register<IParallaxManager, DummyParallaxManager>(true);
|
||||
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
|
||||
IoCManager.Resolve<IConfigurationManager>()
|
||||
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SetDefaultCVars(options);
|
||||
var client = new RobustIntegrationTest.ClientIntegrationInstance(options);
|
||||
await client.WaitIdleAsync();
|
||||
await SetupCVars(client, poolSettings);
|
||||
return (client, logHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Pair.TestPair"/>, which can be used to get access to a server, and client <see cref="Pair.TestPair"/>
|
||||
/// </summary>
|
||||
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<TestPair> GetServerClient(PoolSettings? poolSettings = null)
|
||||
{
|
||||
return await GetServerClientPair(poolSettings ?? new PoolSettings());
|
||||
}
|
||||
|
||||
private static string GetDefaultTestName(TestContext testContext)
|
||||
{
|
||||
return testContext.Test.FullName.Replace("Content.IntegrationTests.Tests.", "");
|
||||
}
|
||||
|
||||
private static async Task<TestPair> GetServerClientPair(PoolSettings poolSettings)
|
||||
{
|
||||
if (!_initialized)
|
||||
throw new InvalidOperationException($"Pool manager has not been initialized");
|
||||
|
||||
// Trust issues with the AsyncLocal that backs this.
|
||||
var testContext = TestContext.CurrentContext;
|
||||
var testOut = TestContext.Out;
|
||||
|
||||
DieIfPoolFailure();
|
||||
var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
|
||||
var poolRetrieveTimeWatch = new Stopwatch();
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
|
||||
TestPair? pair = null;
|
||||
try
|
||||
{
|
||||
poolRetrieveTimeWatch.Start();
|
||||
if (poolSettings.MustBeNew)
|
||||
{
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
|
||||
pair = await CreateServerClientPair(poolSettings, testOut);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
|
||||
pair = GrabOptimalPair(poolSettings);
|
||||
if (pair != null)
|
||||
{
|
||||
pair.ActivateContext(testOut);
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
|
||||
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
|
||||
|
||||
if (canSkip)
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
|
||||
await SetupCVars(pair.Client, poolSettings);
|
||||
await SetupCVars(pair.Server, poolSettings);
|
||||
await pair.RunTicksSync(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
|
||||
await pair.CleanPooledPair(poolSettings, testOut);
|
||||
}
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.SyncTicks(targetDelta: 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
|
||||
pair = await CreateServerClientPair(poolSettings, testOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pair != null && pair.TestHistory.Count > 0)
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
|
||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
||||
{
|
||||
await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
|
||||
}
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History End");
|
||||
}
|
||||
}
|
||||
|
||||
pair.ValidateSettings(poolSettings);
|
||||
|
||||
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
|
||||
|
||||
pair.ClearModifiedCvars();
|
||||
pair.Settings = poolSettings;
|
||||
pair.TestHistory.Add(currentTestName);
|
||||
pair.SetupSeed();
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
|
||||
|
||||
pair.Watch.Restart();
|
||||
return pair;
|
||||
}
|
||||
|
||||
private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
|
||||
{
|
||||
lock (PairLock)
|
||||
{
|
||||
TestPair? fallback = null;
|
||||
foreach (var pair in Pairs.Keys)
|
||||
{
|
||||
if (Pairs[pair])
|
||||
continue;
|
||||
|
||||
if (!pair.Settings.CanFastRecycle(poolSettings))
|
||||
{
|
||||
fallback = pair;
|
||||
continue;
|
||||
}
|
||||
|
||||
pair.Use();
|
||||
Pairs[pair] = true;
|
||||
return pair;
|
||||
}
|
||||
|
||||
if (fallback != null)
|
||||
{
|
||||
fallback.Use();
|
||||
Pairs[fallback!] = true;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by TestPair after checking the server/client pair, Don't use this.
|
||||
/// </summary>
|
||||
public static void NoCheckReturn(TestPair pair)
|
||||
{
|
||||
lock (PairLock)
|
||||
{
|
||||
if (pair.State == TestPair.PairState.Dead)
|
||||
Pairs.Remove(pair);
|
||||
else if (pair.State == TestPair.PairState.Ready)
|
||||
Pairs[pair] = false;
|
||||
else
|
||||
throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DieIfPoolFailure()
|
||||
{
|
||||
if (_poolFailureReason != null)
|
||||
{
|
||||
// If the _poolFailureReason is not null, we can assume at least one test failed.
|
||||
// So we say inconclusive so we don't add more failed tests to search through.
|
||||
Assert.Inconclusive(@$"
|
||||
In a different test, the pool manager had an exception when trying to create a server/client pair.
|
||||
Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
|
||||
we are just going to end this here to save a lot of time. This is the exception that started this:\n {_poolFailureReason}");
|
||||
}
|
||||
|
||||
if (_dead)
|
||||
{
|
||||
// If Pairs is null, we ran out of time, we can't assume a test failed.
|
||||
// So we are going to tell it all future tests are a failure.
|
||||
Assert.Fail("The pool was shut down");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<TestPair> CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = Interlocked.Increment(ref _pairId);
|
||||
var pair = new TestPair(id);
|
||||
await pair.Initialize(poolSettings, testOut, _testPrototypes);
|
||||
pair.Use();
|
||||
await pair.RunTicksSync(5);
|
||||
await pair.SyncTicks(targetDelta: 1);
|
||||
return pair;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_poolFailureReason = ex;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a server, or a client until a condition is true
|
||||
@@ -418,29 +67,42 @@ we are just going to end this here to save a lot of time. This is the exception
|
||||
Assert.That(passed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the pool manager.
|
||||
/// </summary>
|
||||
/// <param name="extraAssemblies">Assemblies to search for to discover extra prototypes and systems.</param>
|
||||
public static void Startup(params Assembly[] extraAssemblies)
|
||||
public static async Task<TestPair> GetServerClient(
|
||||
PoolSettings? settings = null,
|
||||
ITestContextLike? testContext = null)
|
||||
{
|
||||
if (_initialized)
|
||||
throw new InvalidOperationException("Already initialized");
|
||||
return await Instance.GetPair(settings, testContext);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
_contentAssemblies =
|
||||
[
|
||||
typeof(Shared.Entry.EntryPoint).Assembly,
|
||||
typeof(Server.Entry.EntryPoint).Assembly,
|
||||
typeof(PoolManager).Assembly
|
||||
];
|
||||
_contentAssemblies.UnionWith(extraAssemblies);
|
||||
public static void Startup(params Assembly[] extra)
|
||||
=> Instance.Startup(extra);
|
||||
|
||||
_testPrototypes.Clear();
|
||||
DiscoverTestPrototypes(typeof(PoolManager).Assembly);
|
||||
foreach (var assembly in extraAssemblies)
|
||||
{
|
||||
DiscoverTestPrototypes(assembly);
|
||||
}
|
||||
public static void Shutdown() => Instance.Shutdown();
|
||||
public static string DeathReport() => Instance.DeathReport();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
|
||||
/// </summary>
|
||||
public sealed class ContentPoolManager : PoolManager<TestPair>
|
||||
{
|
||||
public override PairSettings DefaultSettings => new PoolSettings();
|
||||
protected override string GetDefaultTestName(ITestContextLike testContext)
|
||||
{
|
||||
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
|
||||
}
|
||||
|
||||
public override void Startup(params Assembly[] extraAssemblies)
|
||||
{
|
||||
DefaultCvars.AddRange(PoolManager.TestCvars);
|
||||
|
||||
var shared = extraAssemblies
|
||||
.Append(typeof(Shared.Entry.EntryPoint).Assembly)
|
||||
.Append(typeof(PoolManager).Assembly)
|
||||
.ToArray();
|
||||
|
||||
Startup([typeof(Client.Entry.EntryPoint).Assembly],
|
||||
[typeof(Server.Entry.EntryPoint).Assembly],
|
||||
shared);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,31 @@
|
||||
#nullable enable
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the pooled server, and client pair.
|
||||
/// Some options are for changing the pair, and others are
|
||||
/// so the pool can properly clean up what you borrowed.
|
||||
/// </summary>
|
||||
public sealed class PoolSettings
|
||||
/// <inheritdoc/>
|
||||
public sealed class PoolSettings : PairSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Set to true if the test will ruin the server/client pair.
|
||||
/// </summary>
|
||||
public bool Destructive { get; init; }
|
||||
public override bool Connected
|
||||
{
|
||||
get => _connected || InLobby;
|
||||
init => _connected = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be created fresh.
|
||||
/// </summary>
|
||||
public bool Fresh { get; init; }
|
||||
private readonly bool _dummyTicker = true;
|
||||
private readonly bool _connected;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
|
||||
/// </summary>
|
||||
public bool DummyTicker { get; init; } = true;
|
||||
public bool DummyTicker
|
||||
{
|
||||
get => _dummyTicker && !InLobby;
|
||||
init => _dummyTicker = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, this enables the creation of admin logs during the test.
|
||||
/// </summary>
|
||||
public bool AdminLogsEnabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be connected from each other.
|
||||
/// Defaults to disconnected as it makes dirty recycling slightly faster.
|
||||
/// If <see cref="InLobby"/> is true, this option is ignored.
|
||||
/// </summary>
|
||||
public bool Connected { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be in the lobby.
|
||||
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
|
||||
@@ -53,89 +41,22 @@ public sealed class PoolSettings
|
||||
/// </summary>
|
||||
public bool NoLoadContent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// This will return a server-client pair that has not loaded test prototypes.
|
||||
/// Try avoiding this whenever possible, as this will always create & destroy a new pair.
|
||||
/// Use <see cref="Pair.TestPair.IsTestPrototype(Robust.Shared.Prototypes.EntityPrototype)"/> if you need to exclude test prototypees.
|
||||
/// </summary>
|
||||
public bool NoLoadTestPrototypes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to disable the NetInterp CVar on the given server/client pair
|
||||
/// </summary>
|
||||
public bool DisableInterpolate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to always clean up the server/client pair before giving it to another borrower
|
||||
/// </summary>
|
||||
public bool Dirty { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to the path of a map to have the given server/client pair load the map.
|
||||
/// </summary>
|
||||
public string Map { get; init; } = PoolManager.TestMap;
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the test name detection, and uses this in the test history instead
|
||||
/// </summary>
|
||||
public string? TestName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public int? ServerSeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public int? ClientSeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Frontier: the preset to run the game in.
|
||||
/// Set to secret for upstream tests to mimic upstream behaviour.
|
||||
/// If you need to check adventure game rule things, set this to nfadventure or nfpirate.
|
||||
/// </summary>
|
||||
public string GameLobbyDefaultPreset { get; set; } = "secret";
|
||||
|
||||
#region Inferred Properties
|
||||
|
||||
/// <summary>
|
||||
/// If the returned pair must not be reused
|
||||
/// </summary>
|
||||
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must be brand new
|
||||
/// </summary>
|
||||
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
public bool UseDummyTicker => !InLobby && DummyTicker;
|
||||
|
||||
public bool ShouldBeConnected => InLobby || Connected;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tries to guess if we can skip recycling the server/client pair.
|
||||
/// </summary>
|
||||
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
|
||||
/// <returns>If we can skip cleaning it up</returns>
|
||||
public bool CanFastRecycle(PoolSettings nextSettings)
|
||||
public override bool CanFastRecycle(PairSettings nextSettings)
|
||||
{
|
||||
if (MustNotBeReused)
|
||||
throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
|
||||
if (!base.CanFastRecycle(nextSettings))
|
||||
return false;
|
||||
|
||||
if (nextSettings.MustBeNew)
|
||||
throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
|
||||
|
||||
if (Dirty)
|
||||
if (nextSettings is not PoolSettings next)
|
||||
return false;
|
||||
|
||||
// Check that certain settings match.
|
||||
return !ShouldBeConnected == !nextSettings.ShouldBeConnected
|
||||
&& UseDummyTicker == nextSettings.UseDummyTicker
|
||||
&& Map == nextSettings.Map
|
||||
&& InLobby == nextSettings.InLobby
|
||||
&& GameLobbyDefaultPreset == nextSettings.GameLobbyDefaultPreset; // Frontier: swappable presets
|
||||
return DummyTicker == next.DummyTicker
|
||||
&& Map == next.Map
|
||||
&& InLobby == next.InLobby;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
using System.IO;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Log handler intended for pooled integration tests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class logs to two places: an NUnit <see cref="TestContext"/>
|
||||
/// (so it nicely gets attributed to a test in your IDE),
|
||||
/// and an in-memory ring buffer for diagnostic purposes.
|
||||
/// If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class PoolTestLogHandler : ILogHandler
|
||||
{
|
||||
private readonly string? _prefix;
|
||||
|
||||
private RStopwatch _stopwatch;
|
||||
|
||||
public TextWriter? ActiveContext { get; private set; }
|
||||
|
||||
public LogLevel? FailureLevel { get; set; }
|
||||
|
||||
public PoolTestLogHandler(string? prefix)
|
||||
{
|
||||
_prefix = prefix != null ? $"{prefix}: " : "";
|
||||
}
|
||||
|
||||
public bool ShuttingDown;
|
||||
|
||||
public void Log(string sawmillName, LogEvent message)
|
||||
{
|
||||
var level = message.Level.ToRobust();
|
||||
|
||||
if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
|
||||
return;
|
||||
|
||||
if (ActiveContext is not { } testContext)
|
||||
{
|
||||
// If this gets hit it means something is logging to this instance while it's "between" tests.
|
||||
// This is a bug in either the game or the testing system, and must always be investigated.
|
||||
throw new InvalidOperationException("Log to pool test log handler without active test context");
|
||||
}
|
||||
|
||||
var name = LogMessage.LogLevelToName(level);
|
||||
var seconds = _stopwatch.Elapsed.TotalSeconds;
|
||||
var rendered = message.RenderMessage();
|
||||
var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}";
|
||||
|
||||
testContext.WriteLine(line);
|
||||
|
||||
if (FailureLevel == null || level < FailureLevel)
|
||||
return;
|
||||
|
||||
testContext.Flush();
|
||||
Assert.Fail($"{line} Exception: {message.Exception}");
|
||||
}
|
||||
|
||||
public void ClearContext()
|
||||
{
|
||||
ActiveContext = null;
|
||||
}
|
||||
|
||||
public void ActivateContext(TextWriter context)
|
||||
{
|
||||
_stopwatch.Restart();
|
||||
ActiveContext = context;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
[MeansImplicitUse]
|
||||
public sealed class TestPrototypesAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
@@ -41,6 +42,7 @@ namespace Content.IntegrationTests.Tests
|
||||
.Where(p => !pair.IsTestPrototype(p))
|
||||
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
|
||||
.Where(p => !p.Components.ContainsKey("RoomFill")) // This comp can delete all entities, and spawn others
|
||||
.Where(p => p.Categories.All(x => x.ID != SpawnerCategory)) // mono
|
||||
.Select(p => p.ID)
|
||||
.ToList();
|
||||
|
||||
@@ -55,7 +57,7 @@ namespace Content.IntegrationTests.Tests
|
||||
}
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
@@ -105,6 +107,7 @@ namespace Content.IntegrationTests.Tests
|
||||
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
|
||||
.Where(p => !p.Components.ContainsKey("RoomFill")) // This comp can delete all entities, and spawn others
|
||||
.Where(p => !p.Components.ContainsKey("GridSpawner")) // Mono - We shouldn't spawn grids.
|
||||
.Where(p => p.Categories.All(x => x.ID != SpawnerCategory)) // mono
|
||||
.Select(p => p.ID)
|
||||
.ToList();
|
||||
foreach (var protoId in protoIds)
|
||||
@@ -112,7 +115,7 @@ namespace Content.IntegrationTests.Tests
|
||||
SpawnEntity(entityMan, protoId, map.GridCoords); // Monolith
|
||||
}
|
||||
});
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
|
||||
@@ -166,6 +169,7 @@ namespace Content.IntegrationTests.Tests
|
||||
.Where(p => !p.Abstract)
|
||||
.Where(p => !pair.IsTestPrototype(p))
|
||||
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
|
||||
.Where(p => p.Categories.All(x => x.ID != SpawnerCategory)) // mono
|
||||
.Select(p => p.ID)
|
||||
.ToList();
|
||||
|
||||
@@ -275,74 +279,104 @@ namespace Content.IntegrationTests.Tests
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));
|
||||
|
||||
foreach (var protoId in protoIds)
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
// TODO fix ninja
|
||||
// Currently ninja fails to equip their own loadout.
|
||||
if (protoId == "MobHumanSpaceNinja")
|
||||
continue;
|
||||
|
||||
var count = Count(server.EntMan);
|
||||
var clientCount = Count(client.EntMan);
|
||||
EntityUid uid = default;
|
||||
await server.WaitPost(() => uid = SpawnEntity(server.EntMan, protoId, coords)); // Monolith
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
// If the entity deleted itself, check that it didn't spawn other entities
|
||||
if (!server.EntMan.EntityExists(uid))
|
||||
foreach (var protoId in protoIds)
|
||||
{
|
||||
if (Count(server.EntMan) != count)
|
||||
var count = Count(server.EntMan);
|
||||
var clientCount = Count(client.EntMan);
|
||||
var serverEntities = new HashSet<EntityUid>(Entities(server.EntMan));
|
||||
var clientEntities = new HashSet<EntityUid>(Entities(client.EntMan));
|
||||
EntityUid uid = default;
|
||||
await server.WaitPost(() => uid = SpawnEntity(server.EntMan, protoId, coords)); // Monolith
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
// If the entity deleted itself, check that it didn't spawn other entities
|
||||
if (!server.EntMan.EntityExists(uid))
|
||||
{
|
||||
Assert.Fail($"Server prototype {protoId} failed on deleting itself");
|
||||
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deleting itself\n" +
|
||||
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
|
||||
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deleting itself\n" +
|
||||
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
|
||||
$"Server count was {count}.\n" +
|
||||
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Count(client.EntMan) != clientCount)
|
||||
{
|
||||
Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" +
|
||||
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
|
||||
$"Server was {count}.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Check that the number of entities has increased.
|
||||
Assert.That(Count(server.EntMan), Is.GreaterThan(count), $"Server prototype {protoId} failed on spawning as entity count didn't increase\n" +
|
||||
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
|
||||
Assert.That(Count(client.EntMan), Is.GreaterThan(clientCount), $"Client prototype {protoId} failed on spawning as entity count didn't increase\n" +
|
||||
$"Expected at least {clientCount} and found {client.EntMan.EntityCount}. " +
|
||||
$"Server count was {count}.\n" +
|
||||
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
|
||||
|
||||
// Check that the number of entities has increased.
|
||||
if (Count(server.EntMan) <= count)
|
||||
{
|
||||
Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase");
|
||||
}
|
||||
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
if (Count(client.EntMan) <= clientCount)
|
||||
{
|
||||
Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" +
|
||||
$"Expected at least {clientCount} and found {Count(client.EntMan)}. " +
|
||||
$"Server was {count}");
|
||||
// Check that the number of entities has gone back to the original value.
|
||||
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
|
||||
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
|
||||
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
|
||||
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
|
||||
$"Server count was {count}.\n" +
|
||||
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
|
||||
}
|
||||
|
||||
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
// Check that the number of entities has gone back to the original value.
|
||||
if (Count(server.EntMan) != count)
|
||||
{
|
||||
// Frontier: add expected vs. actual
|
||||
Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly:\n" +
|
||||
$"Expected {count} and found {server.EntMan.EntityCount}.\n");
|
||||
// End Frontier
|
||||
}
|
||||
|
||||
if (Count(client.EntMan) != clientCount)
|
||||
{
|
||||
Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" +
|
||||
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
|
||||
$"Server was {count}.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private static string BuildDiffString(IEnumerable<EntityUid> oldEnts, IEnumerable<EntityUid> newEnts, IEntityManager entMan)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var addedEnts = newEnts.Except(oldEnts);
|
||||
var removedEnts = oldEnts.Except(newEnts);
|
||||
if (addedEnts.Any())
|
||||
sb.AppendLine("Listing new entities:");
|
||||
foreach (var addedEnt in addedEnts)
|
||||
{
|
||||
sb.AppendLine(entMan.ToPrettyString(addedEnt));
|
||||
}
|
||||
if (removedEnts.Any())
|
||||
sb.AppendLine("Listing removed entities:");
|
||||
foreach (var removedEnt in removedEnts)
|
||||
{
|
||||
sb.AppendLine("\t" + entMan.ToPrettyString(removedEnt));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool HasRequiredDataField(Component component)
|
||||
{
|
||||
foreach (var field in component.GetType().GetFields())
|
||||
{
|
||||
foreach (var attribute in field.GetCustomAttributes(true))
|
||||
{
|
||||
if (attribute is not DataFieldAttribute dataField)
|
||||
continue;
|
||||
|
||||
if (dataField.Required)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
foreach (var property in component.GetType().GetProperties())
|
||||
{
|
||||
foreach (var attribute in property.GetCustomAttributes(true))
|
||||
{
|
||||
if (attribute is not DataFieldAttribute dataField)
|
||||
continue;
|
||||
|
||||
if (dataField.Required)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AllComponentsOneToOneDeleteTest()
|
||||
{
|
||||
@@ -367,11 +401,9 @@ namespace Content.IntegrationTests.Tests
|
||||
"ActivatableUI", // Frontier: Requires enum key
|
||||
"AlertLevel", // Frontier: requires alert set
|
||||
"BluespaceErrorRule", // Frontier
|
||||
"GridSpawner", // mono - i wouldn't
|
||||
};
|
||||
|
||||
// TODO TESTS
|
||||
// auto ignore any components that have a "required" data field.
|
||||
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
@@ -389,9 +421,12 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
foreach (var type in componentFactory.AllRegisteredTypes)
|
||||
{
|
||||
var component = (Component) componentFactory.GetComponent(type);
|
||||
var component = (Component)componentFactory.GetComponent(type);
|
||||
var name = componentFactory.GetComponentName(type);
|
||||
|
||||
if (HasRequiredDataField(component))
|
||||
continue;
|
||||
|
||||
// If this component is ignored
|
||||
if (skipComponents.Contains(name))
|
||||
{
|
||||
@@ -428,9 +463,9 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
private EntityUid SpawnEntity(
|
||||
IEntityManager entManager,
|
||||
string? protoName,
|
||||
string protoName,
|
||||
MapCoordinates coordinates,
|
||||
ComponentRegistry? registry = null)
|
||||
ComponentRegistry registry = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -447,9 +482,9 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
private EntityUid SpawnEntity(
|
||||
IEntityManager entManager,
|
||||
string? protoName,
|
||||
string protoName,
|
||||
EntityCoordinates coordinates,
|
||||
ComponentRegistry? registry = null)
|
||||
ComponentRegistry registry = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class StorageInteractionTest : InteractionTest
|
||||
await ClickControl(ctrl, ContentKeyFunctions.ActivateItemInWorld);
|
||||
await RunTicks(10);
|
||||
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
|
||||
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
|
||||
// Assert.That(IsUiOpen(PdaUiKey.Key), Is.True); // Mono heisenfail - TODO: fix
|
||||
|
||||
// Click on the pda to pick it up and remove it from the backpack.
|
||||
await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
|
||||
@@ -71,7 +71,7 @@ public sealed class StorageInteractionTest : InteractionTest
|
||||
|
||||
// UIs should still be open
|
||||
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
|
||||
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
|
||||
// Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class AdminTest : ToolshedTest
|
||||
var toolMan = Server.ResolveDependency<ToolshedManager>();
|
||||
var admin = Server.ResolveDependency<IAdminManager>();
|
||||
var ignored = new HashSet<Assembly>()
|
||||
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
|
||||
{typeof(LocTest).Assembly};
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
@@ -25,6 +25,11 @@ public sealed class AdminTest : ToolshedTest
|
||||
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
|
||||
continue;
|
||||
|
||||
// Only care about content commands.
|
||||
var assemblyName = cmd.Cmd.GetType().Assembly.FullName;
|
||||
if (assemblyName == null || !assemblyName.StartsWith("Content."))
|
||||
continue;
|
||||
|
||||
Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class LocTest : ToolshedTest
|
||||
var locStrings = new HashSet<string>();
|
||||
|
||||
var ignored = new HashSet<Assembly>()
|
||||
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
|
||||
{typeof(LocTest).Assembly};
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
|
||||
@@ -3,19 +3,27 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputPath>..\bin\Content.MapRenderer\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.UnitTesting\Robust.UnitTesting.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Server.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Testing.props" />
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,8 +9,10 @@ using Content.Server.Maps;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -37,6 +39,8 @@ namespace Content.MapRenderer.Painters
|
||||
// Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time.
|
||||
Map = map,
|
||||
});
|
||||
pair.ServerLogHandler.FailureLevel = LogLevel.Fatal;
|
||||
pair.ClientLogHandler.FailureLevel = LogLevel.Fatal;
|
||||
|
||||
await foreach (var image in RenderPair(stopwatch, pair))
|
||||
yield return image;
|
||||
@@ -54,6 +58,8 @@ namespace Content.MapRenderer.Painters
|
||||
Connected = true,
|
||||
Fresh = false,
|
||||
});
|
||||
pair.ServerLogHandler.FailureLevel = LogLevel.Fatal;
|
||||
pair.ClientLogHandler.FailureLevel = LogLevel.Fatal;
|
||||
|
||||
var server = pair.Server;
|
||||
var client = pair.Client;
|
||||
@@ -67,8 +73,27 @@ namespace Content.MapRenderer.Painters
|
||||
|
||||
foreach (var grid in sMapManager.GetAllGrids(mapId))
|
||||
sEntityManager.QueueDeleteEntity(grid);
|
||||
});
|
||||
|
||||
mapLoader.TryLoadGrid(mapId, new(path), out _);
|
||||
await pair.RunTicksSync(10);
|
||||
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var mapId = sEntityManager.System<GameTicker>().DefaultMap;
|
||||
|
||||
try
|
||||
{
|
||||
mapLoader.TryLoadGrid(mapId, new(path), out _);
|
||||
}
|
||||
catch (Exception e) // we probably tried to load a map
|
||||
{
|
||||
Logger.Info($"Failed to load as grid, rendering as map...");
|
||||
sMapManager.DeleteMap(mapId);
|
||||
var opts = new DeserializationOptions();
|
||||
opts.InitializeMaps = true;
|
||||
mapLoader.TryLoadMapWithId(mapId, new(path), out _, out _, opts);
|
||||
}
|
||||
});
|
||||
|
||||
await foreach (var image in RenderPair(stopwatch, pair))
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ServerGarbageCollection>True</ServerGarbageCollection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Packaging\Robust.Packaging.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Packaging.props" />
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Content.Packaging;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for working with <c>.deps.json</c> files.
|
||||
/// </summary>
|
||||
public sealed class DepsHandler
|
||||
{
|
||||
public readonly Dictionary<string, LibraryInfo> Libraries = new();
|
||||
|
||||
public DepsHandler(DepsData data)
|
||||
{
|
||||
if (data.Targets.Count != 1)
|
||||
throw new Exception("Expected exactly one target");
|
||||
|
||||
var target = data.Targets.Single().Value;
|
||||
|
||||
foreach (var (libNameAndVersion, libInfo) in target)
|
||||
{
|
||||
var split = libNameAndVersion.Split('/', 2);
|
||||
|
||||
Libraries.Add(split[0], libInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static DepsHandler Load(string depsFile)
|
||||
{
|
||||
using var f = File.OpenRead(depsFile);
|
||||
var depsData = JsonSerializer.Deserialize<DepsData>(f) ?? throw new InvalidOperationException("Deps are null!");
|
||||
|
||||
return new DepsHandler(depsData);
|
||||
}
|
||||
|
||||
public HashSet<string> RecursiveGetLibrariesFrom(string start)
|
||||
{
|
||||
var found = new HashSet<string>();
|
||||
|
||||
RecursiveAddLibraries(start, found);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private void RecursiveAddLibraries(string start, HashSet<string> set)
|
||||
{
|
||||
if (!set.Add(start))
|
||||
return;
|
||||
|
||||
var lib = Libraries[start];
|
||||
if (lib.Dependencies == null)
|
||||
return;
|
||||
|
||||
foreach (var dep in lib.Dependencies.Keys)
|
||||
{
|
||||
RecursiveAddLibraries(dep, set);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DepsData
|
||||
{
|
||||
[JsonInclude, JsonPropertyName("targets")]
|
||||
public required Dictionary<string, Dictionary<string, LibraryInfo>> Targets;
|
||||
}
|
||||
|
||||
public sealed class LibraryInfo
|
||||
{
|
||||
[JsonInclude, JsonPropertyName("dependencies")]
|
||||
public Dictionary<string, string>? Dependencies;
|
||||
|
||||
[JsonInclude, JsonPropertyName("runtime")]
|
||||
public Dictionary<string, object>? Runtime;
|
||||
|
||||
// Paths are like lib/netstandard2.0/JetBrains.Annotations.dll
|
||||
public IEnumerable<string> GetDllNames()
|
||||
{
|
||||
return Runtime == null ? [] : Runtime.Keys.Select(p => p.Split('/')[^1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,15 @@ public static class ServerPackaging
|
||||
new PlatformReg("osx-x64", "MacOS", true),
|
||||
new PlatformReg("osx-arm64", "MacOS", true),
|
||||
// Non-default platforms (i.e. for Watchdog Git)
|
||||
new PlatformReg("win-x86", "Windows", false),
|
||||
new PlatformReg("linux-x86", "Linux", false),
|
||||
new PlatformReg("linux-arm", "Linux", false),
|
||||
new PlatformReg("freebsd-x64", "FreeBSD", false),
|
||||
};
|
||||
|
||||
private static IReadOnlySet<string> ServerContentIgnoresResources { get; } = new HashSet<string>
|
||||
{
|
||||
"ServerInfo",
|
||||
"Changelog",
|
||||
};
|
||||
|
||||
private static List<string> PlatformRids => Platforms
|
||||
.Select(o => o.Rid)
|
||||
.ToList();
|
||||
@@ -34,25 +37,9 @@ public static class ServerPackaging
|
||||
.Select(o => o.Rid)
|
||||
.ToList();
|
||||
|
||||
private static readonly List<string> ServerContentAssemblies = new()
|
||||
{
|
||||
"Content.Server.Database",
|
||||
"Content.Server",
|
||||
"Content.Shared",
|
||||
"Content.Shared.Database",
|
||||
};
|
||||
|
||||
private static readonly List<string> ServerExtraAssemblies = new()
|
||||
{
|
||||
// Python script had Npgsql. though we want Npgsql.dll as well soooo
|
||||
"Npgsql",
|
||||
"Microsoft",
|
||||
"Discord",
|
||||
};
|
||||
|
||||
private static readonly List<string> ServerNotExtraAssemblies = new()
|
||||
{
|
||||
"Microsoft.CodeAnalysis",
|
||||
"JetBrains.Annotations",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> BinSkipFolders = new()
|
||||
@@ -178,23 +165,13 @@ public static class ServerPackaging
|
||||
|
||||
var inputPassCore = graph.InputCore;
|
||||
var inputPassResources = graph.InputResources;
|
||||
var contentAssemblies = new List<string>(ServerContentAssemblies);
|
||||
|
||||
// Additional assemblies that need to be copied such as EFCore.
|
||||
var sourcePath = Path.Combine(contentDir, "bin", "Content.Server");
|
||||
|
||||
// Should this be an asset pass?
|
||||
// For future archaeologists I just want audio rework to work and need the audio pass so
|
||||
// just porting this as is from python.
|
||||
foreach (var fullPath in Directory.EnumerateFiles(sourcePath, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(fullPath);
|
||||
var deps = DepsHandler.Load(Path.Combine(sourcePath, "Content.Server.deps.json"));
|
||||
|
||||
if (!ServerNotExtraAssemblies.Any(o => fileName.StartsWith(o)) && ServerExtraAssemblies.Any(o => fileName.StartsWith(o)))
|
||||
{
|
||||
contentAssemblies.Add(fileName);
|
||||
}
|
||||
}
|
||||
var contentAssemblies = GetContentAssemblyNamesToCopy(deps);
|
||||
|
||||
await RobustSharedPackaging.DoResourceCopy(
|
||||
Path.Combine("RobustToolbox", "bin", "Server",
|
||||
@@ -211,7 +188,11 @@ public static class ServerPackaging
|
||||
contentAssemblies,
|
||||
cancel: cancel);
|
||||
|
||||
await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
|
||||
await RobustServerPackaging.WriteServerResources(
|
||||
contentDir,
|
||||
inputPassResources,
|
||||
ServerContentIgnoresResources.Concat(RobustSharedPackaging.SharedIgnoredResources).ToHashSet(),
|
||||
cancel);
|
||||
|
||||
if (hybridAcz)
|
||||
{
|
||||
@@ -222,5 +203,21 @@ public static class ServerPackaging
|
||||
inputPassResources.InjectFinished();
|
||||
}
|
||||
|
||||
// This returns both content assemblies (e.g. Content.Server.dll) and dependencies (e.g. Npgsql)
|
||||
private static IEnumerable<string> GetContentAssemblyNamesToCopy(DepsHandler deps)
|
||||
{
|
||||
var depsContent = deps.RecursiveGetLibrariesFrom("Content.Server").SelectMany(GetLibraryNames);
|
||||
var depsRobust = deps.RecursiveGetLibrariesFrom("Robust.Server").SelectMany(GetLibraryNames);
|
||||
|
||||
var depsContentExclusive = depsContent.Except(depsRobust).ToHashSet();
|
||||
|
||||
// Remove .dll suffix and apply filtering.
|
||||
var names = depsContentExclusive.Select(p => p[..^4]).Where(p => !ServerNotExtraAssemblies.Any(p.StartsWith));
|
||||
|
||||
return names;
|
||||
|
||||
IEnumerable<string> GetLibraryNames(string library) => deps.Libraries[library].GetDllNames();
|
||||
}
|
||||
|
||||
private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>..\bin\Content.Replay\</OutputPath>
|
||||
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nett" />
|
||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputPath>..\bin\Content.Server.Database\</OutputPath>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>RA0003</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class PanicBunkerCommand : LocalizedCommands
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
shell.WriteError(Robust.Shared.Localization.Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1)));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class PanicBunkerCommand : LocalizedCommands
|
||||
|
||||
if (args.Length == 1 && !bool.TryParse(args[0], out enabled))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
|
||||
shell.WriteError(Robust.Shared.Localization.Loc.GetString("shell-argument-must-be-boolean"));
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,9 @@ public sealed class AmeNodeGroup : BaseNodeGroup
|
||||
// At 18+ cores and 2 inject, the power produced is less than 0, the Max ensures the AME can never produce "negative" power.
|
||||
// return MathF.Max(200000f * MathF.Log10(2 * fuel * MathF.Pow(cores, (float)-0.5)), 0); // Frontier: preferring old calculation for now
|
||||
// return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow(0.75f, cores - 1); // Frontier: preferring old calculation for now
|
||||
return 200000f * MathF.Log10(fuel * fuel); // Mono - Removed diminishing returns from AME core count.
|
||||
if (cores > 0 && fuel > 0) // Mono - default zero power unless conditions are met
|
||||
return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow( 1.11111f, fuel / 2f - 1); // Mono - Exponential scaling at high injection
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetTotalStability()
|
||||
|
||||
@@ -258,7 +258,8 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
if (pressure <= Atmospherics.HazardLowPressure)
|
||||
{
|
||||
// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear.
|
||||
_damageableSystem.TryChangeDamage(uid, barotrauma.Damage * Atmospherics.LowPressureDamage, true, false, origin: uid, canSever: false);
|
||||
// Mono: DamageOriginFlag arg to stop armor plate system mitigation
|
||||
_damageableSystem.TryChangeDamage(uid, barotrauma.Damage * Atmospherics.LowPressureDamage, true, false, origin: uid, canSever: false, originFlag: DamageableSystem.DamageOriginFlag.Barotrauma);
|
||||
|
||||
if (!barotrauma.TakingDamage)
|
||||
{
|
||||
@@ -273,7 +274,8 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
var damageScale = MathF.Min(((pressure / Atmospherics.HazardHighPressure) - 1) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
|
||||
|
||||
// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear.
|
||||
_damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false, origin: uid, canSever: false);
|
||||
// Mono: DamageOriginFlag arg
|
||||
_damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false, origin: uid, canSever: false, originFlag: DamageableSystem.DamageOriginFlag.Barotrauma);
|
||||
|
||||
if (!barotrauma.TakingDamage)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
@@ -13,13 +10,14 @@ using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// ReSharper disable once RedundantUsingDirective
|
||||
|
||||
@@ -31,7 +29,6 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
[Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _confMan = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = default!;
|
||||
@@ -60,12 +57,16 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
private float _updateInterval;
|
||||
|
||||
private int _thresholds;
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
private EntityQuery<GasTileOverlayComponent> _query;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_query = GetEntityQuery<GasTileOverlayComponent>();
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
|
||||
_updateJob = new UpdatePlayerJob()
|
||||
{
|
||||
EntManager = EntityManager,
|
||||
@@ -76,16 +77,15 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
MapManager = _mapManager,
|
||||
ChunkViewerPool = _chunkViewerPool,
|
||||
LastSentChunks = _lastSentChunks,
|
||||
GridQuery = _gridQuery,
|
||||
};
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
|
||||
Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
|
||||
Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true);
|
||||
|
||||
InitializeCVars();
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
|
||||
_query = GetEntityQuery<GasTileOverlayComponent>();
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
|
||||
@@ -170,7 +170,16 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
|
||||
public GasOverlayData GetOverlayData(GasMixture? mixture)
|
||||
{
|
||||
var data = new GasOverlayData(0, 0, new byte[VisibleGasId.Length]);
|
||||
ThermalByte byteTemp;
|
||||
if (mixture == null)
|
||||
{
|
||||
byteTemp = new();
|
||||
byteTemp.SetVacuum();
|
||||
}
|
||||
else
|
||||
byteTemp = new(mixture.Temperature);
|
||||
|
||||
var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp, 0);
|
||||
|
||||
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||
{
|
||||
@@ -210,15 +219,28 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
|
||||
ThermalByte newByteTemp = new();
|
||||
|
||||
if (tile.Hotspot.Valid)
|
||||
newByteTemp.SetTemperature(tile.Hotspot.Temperature);
|
||||
else if (!tile.Space && tile.Air?.TotalMoles <= 5f)
|
||||
newByteTemp.SetVacuum();
|
||||
else if (!tile.Space && tile.Air != null)
|
||||
newByteTemp = new(tile.Air.Temperature);
|
||||
|
||||
if (oldData.Equals(default))
|
||||
{
|
||||
changed = true;
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, (byte) tile.Hotspot.Type, new byte[VisibleGasId.Length]);
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp, (byte)tile.Hotspot.Type);
|
||||
}
|
||||
else if (oldData.FireState != tile.Hotspot.State || oldData.FireType != (byte)tile.Hotspot.Type)
|
||||
else if (oldData.FireState != tile.Hotspot.State ||
|
||||
oldData.FireType != (byte)tile.Hotspot.Type || // mono reagent fire
|
||||
Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average.
|
||||
(oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution)) // change of special ThermalByte value
|
||||
{
|
||||
changed = true;
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, (byte) tile.Hotspot.Type, oldData.Opacity);
|
||||
oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp, (byte)tile.Hotspot.Type);
|
||||
}
|
||||
|
||||
if (tile is {Air: not null, NoGridTile: false})
|
||||
@@ -375,6 +397,8 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
public Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> LastSentChunks;
|
||||
public List<ICommonSession> Sessions;
|
||||
|
||||
public EntityQuery<MapGridComponent> GridQuery;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var playerSession = Sessions[index];
|
||||
@@ -391,7 +415,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
previouslySent.Remove(netGrid);
|
||||
|
||||
// If grid was deleted then don't worry about sending it to the client.
|
||||
if (!EntManager.TryGetEntity(netGrid, out var gridId) || !MapManager.IsGrid(gridId.Value))
|
||||
if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
|
||||
ev.RemovedChunks[netGrid] = oldIndices;
|
||||
else
|
||||
{
|
||||
@@ -458,5 +482,12 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void InitializeCVars()
|
||||
{
|
||||
Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
|
||||
Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
|
||||
Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,16 @@ public sealed partial class ThermalRegulatorComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ThermalRegulationTemperatureThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Process regulation while they're dead if there is a MobStateComponent? - Mono Hydrakin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ProcessWhileDead = false;
|
||||
|
||||
/// <summary>
|
||||
/// Process regulation in critical condition if there is a MobStateComponent? - Mono Hydrakin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ProcessWhileCrit = true;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user