├── .gitattributes
├── .github
└── workflows
│ └── deployment.yml
├── .gitignore
├── Dummy.sln
├── Dummy
├── API
│ ├── Exceptions
│ │ ├── DummyCanceledSpawnException.cs
│ │ ├── DummyContainsException.cs
│ │ └── DummyOverflowsException.cs
│ ├── IAction.cs
│ └── IDummyProvider.cs
├── Actions
│ ├── CustomAction.cs
│ ├── Interaction
│ │ ├── Actions
│ │ │ ├── ExecuteCommandAction.cs
│ │ │ ├── FaceAction.cs
│ │ │ ├── GestureAction.cs
│ │ │ ├── MouseAction.cs
│ │ │ ├── UI
│ │ │ │ ├── ClickButtonAction.cs
│ │ │ │ └── InputTextAction.cs
│ │ │ └── Vehicle
│ │ │ │ ├── EnterVehicleAction.cs
│ │ │ │ └── ExitVehicleAction.cs
│ │ └── MouseState.cs
│ ├── JumpingEx.cs
│ ├── MouseEx.cs
│ ├── Movement
│ │ └── Actions
│ │ │ ├── JumpAction.cs
│ │ │ ├── LookAtAction.cs
│ │ │ ├── RotateAction.cs
│ │ │ ├── SprintAction.cs
│ │ │ ├── StanceAction.cs
│ │ │ └── StrafeAction.cs
│ ├── StrafeDirection.cs
│ ├── Strafing.cs
│ └── SwappingFace.cs
├── Commands
│ ├── Actions
│ │ ├── CommandDummyButton.cs
│ │ ├── CommandDummyExecute.cs
│ │ ├── CommandDummyFace.cs
│ │ ├── CommandDummyGesture.cs
│ │ ├── CommandDummyInputText.cs
│ │ ├── CommandDummyJump.cs
│ │ ├── CommandDummyLook.cs
│ │ ├── CommandDummyMouse.cs
│ │ ├── CommandDummyMove.cs
│ │ ├── CommandDummyRide.cs
│ │ └── CommandDummyStance.cs
│ ├── CommandDummy.cs
│ ├── CommandDummyAction.cs
│ ├── CommandDummyClear.cs
│ ├── CommandDummyCopy.cs
│ ├── CommandDummyCreate.cs
│ ├── CommandDummyRemove.cs
│ ├── CommandDummySpawn.cs
│ ├── CommandDummyTphere.cs
│ ├── Helpers
│ │ └── CommandArgument.cs
│ └── Tests
│ │ └── CommandTestSpawn.cs
├── ConfigurationEx
│ ├── ColorTypeConverter.cs
│ ├── IPAddressTypeConverter.cs
│ └── NullConfiguration.cs
├── Dummy.cs
├── Dummy.csproj
├── Events
│ ├── DummyDeadEvent.cs
│ ├── DummyPlayerDamageEvent.cs
│ └── DummyServerSendingMessageEvent.cs
├── Extensions
│ ├── ConvertorExtension.cs
│ └── UnturnedExtension.cs
├── Models
│ ├── Clothing.cs
│ ├── Configuration.cs
│ ├── ConfigurationConnection.cs
│ ├── ConfigurationEvents.cs
│ ├── ConfigurationFun.cs
│ ├── ConfigurationOptions.cs
│ └── ConfigurationSettings.cs
├── NetTransports
│ └── DummyTransportConnection.cs
├── Patches
│ ├── Patch_InteractableVehicle.cs
│ ├── Patch_Provider.cs
│ └── Patch_ServerMessageHandler_ReadyToConnect.cs
├── Permissions
│ └── DummyPermissionCheckProvider.cs
├── Players
│ └── DummyPlayer.cs
├── ServiceConfigurator.cs
├── Services
│ ├── DummyProvider.cs
│ └── DummyUserProvider.cs
├── Threads
│ ├── DummyUserActionThread.cs
│ ├── DummyUserSimulationThread.Equipment.cs
│ ├── DummyUserSimulationThread.Movement.cs
│ └── DummyUserSimulationThread.cs
├── Users
│ └── DummyUser.cs
├── config.yaml
└── translations.yaml
├── LICENSE
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/deployment.yml:
--------------------------------------------------------------------------------
1 | name: GitHub release and NuGet deploy
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Plugin Version (SemVer: https://semver.org)'
8 | required: true
9 | update_notes:
10 | default: "-"
11 | description: "Update Notes to Release"
12 | required: true
13 | jobs:
14 | deploy:
15 | name: "NuGet Deployment"
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | name: Checkout Repository
20 | with:
21 | fetch-depth: 0
22 | - name: Setup .NET
23 | uses: actions/setup-dotnet@v1
24 | with:
25 | dotnet-version: 6.0.x
26 | - name: Install dependencies
27 | run: dotnet restore
28 | - name: Update version
29 | run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Dummy/Dummy.csproj"
30 | - name: Update package version
31 | run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Dummy/Dummy.csproj"
32 | - name: Update informational version
33 | run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Dummy/Dummy.csproj"
34 | - name: Build
35 | run: dotnet build Dummy/Dummy.csproj --configuration Release --no-restore
36 | - name: Deploy to NuGet
37 | run: dotnet nuget push Dummy/bin/Release/*.nupkg
38 | --api-key ${{ secrets.NUGET_DEPLOY_KEY }}
39 | --source https://api.nuget.org/v3/index.json
40 | - name: Release to GitHub
41 | uses: actions/create-release@master
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 | with:
45 | body: |
46 | Install plugin with command: `openmod install EvolutionPlugins.Dummy`
47 | NuGet: https://www.nuget.org/packages/EvolutionPlugins.Dummy
48 | Changelog:
49 | ${{ github.event.inputs.update_notes }}
50 | release_name: EvolutionPlugins.Dummy v${{ github.event.inputs.version }}
51 | tag_name: v${{ github.event.inputs.version }}
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/Dummy.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33402.96
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dummy", "Dummy\Dummy.csproj", "{96D2F1A9-FBA4-4DFA-AD38-FC80767961EC}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {96D2F1A9-FBA4-4DFA-AD38-FC80767961EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {96D2F1A9-FBA4-4DFA-AD38-FC80767961EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {96D2F1A9-FBA4-4DFA-AD38-FC80767961EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {96D2F1A9-FBA4-4DFA-AD38-FC80767961EC}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {E9770D8A-6914-4ECD-B13A-2851316518ED}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Dummy/API/Exceptions/DummyCanceledSpawnException.cs:
--------------------------------------------------------------------------------
1 | using OpenMod.API.Commands;
2 |
3 | namespace Dummy.API.Exceptions
4 | {
5 | public class DummyCanceledSpawnException : UserFriendlyException
6 | {
7 | public DummyCanceledSpawnException(string message) : base(message)
8 | {
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Dummy/API/Exceptions/DummyContainsException.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Localization;
2 | using OpenMod.API.Commands;
3 | using System;
4 |
5 | namespace Dummy.API.Exceptions
6 | {
7 | [Serializable]
8 | public class DummyContainsException : UserFriendlyException
9 | {
10 | public ulong Id { get; }
11 |
12 | public DummyContainsException(IStringLocalizer stringLocalizer, ulong id) : base(stringLocalizer["exceptions:contains",
13 | new { Id = id }])
14 | {
15 | Id = id;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/Dummy/API/Exceptions/DummyOverflowsException.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Localization;
2 | using OpenMod.API.Commands;
3 | using System;
4 |
5 | namespace Dummy.API.Exceptions
6 | {
7 | [Serializable]
8 | public class DummyOverflowsException : UserFriendlyException
9 | {
10 | public byte DummiesCount { get; }
11 | public byte MaxDummies { get; }
12 |
13 | public DummyOverflowsException(IStringLocalizer stringLocalizer, byte dummiesCount, byte maxDummies) : base(stringLocalizer[
14 | "exceptions:overflow",
15 | new { DummiesCount = dummiesCount, MaxDummies = maxDummies }])
16 | {
17 | DummiesCount = dummiesCount;
18 | MaxDummies = maxDummies;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/Dummy/API/IAction.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Dummy.Users;
3 |
4 | namespace Dummy.API
5 | {
6 | public interface IAction
7 | {
8 | ///
9 | /// Do a action for dummy.
10 | ///
11 | ///
12 | Task Do(DummyUser dummy);
13 | }
14 | }
--------------------------------------------------------------------------------
/Dummy/API/IDummyProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Dummy.API.Exceptions;
5 | using Dummy.Models;
6 | using Dummy.Users;
7 | using OpenMod.API.Ioc;
8 | using OpenMod.Unturned.Users;
9 | using Steamworks;
10 |
11 | namespace Dummy.API
12 | {
13 | [Service]
14 | public interface IDummyProvider
15 | {
16 | ///
17 | /// The list of dummies
18 | ///
19 | IReadOnlyCollection Dummies { get; }
20 |
21 | ///
22 | /// Spawn a dummy with default settings in config.yaml file
23 | ///
24 | /// The to spawn a dummy. Set to null to make sure the id will be unique
25 | /// Owners get all logs about a dummy
26 | /// Dummy with id already created
27 | /// Dummies limit reached
28 | /// The game or plugin cancelled connection of dummy
29 | Task AddDummyAsync(CSteamID? id, HashSet? owners);
30 |
31 | ///
32 | /// Spawn a dummy and copy all parameters of the user
33 | ///
34 | /// The to spawn a dummy. Set to to make sure the id will be unique
35 | /// The list of owners that get all notification about a dummy
36 | /// The user to copy partially all parameters to dummy
37 | /// Dummy with already created
38 | /// Dummies limit reached
39 | /// The game or plugin cancelled connection of dummy
40 | Task AddCopiedDummyAsync(CSteamID? id, HashSet? owners, UnturnedUser? userCopy);
41 |
42 | ///
43 | /// Spawn a dummy and copy all parameters of the user
44 | ///
45 | /// The steamId to spawn a dummy. Set to to make sure the id will be unique
46 | /// OThe list of owners that get all notification about a dummy
47 | /// Parameters to spawn dummy. If it's then it will use default settings in config.yaml file
48 | /// Dummy with already created
49 | /// Dummies limit reached
50 | /// The game or plugin cancelled connection of dummy
51 | Task AddDummyByParameters(CSteamID? id, HashSet? owners, ConfigurationSettings? settings);
52 |
53 | ///
54 | /// Spawn a dummy with or try to copy from .
55 | ///
56 | /// NOTE: has higher priority than , so you can change the name of dummy and copy all other parameters from the user
57 | /// The to spawn a dummy. Set to null to make sure the id will be unique
58 | /// The list of owners that get all notification about a dummy
59 | /// The user to copy partially all parameters to dummy
60 | /// Parameters to spawn dummy. If it's then it will use default settings in config.yaml file
61 | /// Dummy with already created
62 | /// Dummies limit reached
63 | /// The game or plugin cancelled connection of dummy
64 | Task AddDummyAsync(CSteamID? id, HashSet? owners = null, UnturnedUser? userCopy = null, ConfigurationSettings? settings = null);
65 |
66 | ///
67 | /// Remove a dummy by
68 | ///
69 | /// The steamId of dummy to remove
70 | /// Returns if dummy removed; otherwise
71 | Task RemoveDummyAsync(CSteamID id);
72 |
73 | ///
74 | /// Removes all dummies
75 | ///
76 | Task ClearDummiesAsync();
77 |
78 | [Obsolete("Use instead a method that accept CSteamID")]
79 | Task FindDummyUserAsync(ulong id);
80 |
81 | ///
82 | /// Searches a dummy by
83 | ///
84 | /// The steamId of dummy to find it
85 | Task FindDummyUserAsync(CSteamID id);
86 | }
87 | }
--------------------------------------------------------------------------------
/Dummy/Actions/CustomAction.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Dummy.API;
3 | using Dummy.Users;
4 | using System;
5 | using System.Threading.Tasks;
6 | using JetBrainsAnnotations::JetBrains.Annotations;
7 |
8 | namespace Dummy.Actions
9 | {
10 | [UsedImplicitly]
11 | public class CustomAction : IAction
12 | {
13 | public CustomAction(Func func)
14 | {
15 | Func = func;
16 | }
17 |
18 | public Func Func { get; }
19 |
20 | public Task Do(DummyUser dummy)
21 | {
22 | return Func(dummy);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/ExecuteCommandAction.cs:
--------------------------------------------------------------------------------
1 | using Dummy.API;
2 | using Dummy.Users;
3 | using OpenMod.API.Commands;
4 | using System;
5 | using System.Threading.Tasks;
6 | using OpenMod.API;
7 | using OpenMod.API.Eventing;
8 | using OpenMod.Core.Commands.Events;
9 | using OpenMod.Core.Eventing;
10 | using OpenMod.Core.Helpers;
11 |
12 | namespace Dummy.Actions.Interaction.Actions
13 | {
14 | public class ExecuteCommandAction : IAction
15 | {
16 | private readonly ICommandExecutor m_CommandExecutor;
17 | private readonly IOpenModComponent? m_Component;
18 | private readonly IEventBus? m_EventBus;
19 | private readonly Action? m_CommandExecutedHandler;
20 |
21 | private DummyUser? m_User;
22 | private bool m_ExceptionHandled;
23 |
24 | public string[] Arguments { get; }
25 |
26 | public ExecuteCommandAction(ICommandExecutor commandExecutor, string[] args, IOpenModComponent? component,
27 | IEventBus? eventBus, Action? commandExecutedHandler)
28 | {
29 | Arguments = args;
30 | m_CommandExecutor = commandExecutor;
31 | m_Component = component;
32 | m_EventBus = eventBus;
33 | m_CommandExecutedHandler = commandExecutedHandler;
34 | }
35 |
36 | public Task Do(DummyUser dummy)
37 | {
38 | AsyncHelper.Schedule("dummy-execute-command", async () =>
39 | {
40 | m_User = dummy;
41 | IDisposable iEvent = NullDisposable.Instance;
42 | if (m_EventBus != null && m_Component != null)
43 | {
44 | iEvent = m_EventBus.Subscribe(m_Component, OnCommandExecutedEvent);
45 | }
46 |
47 | var commandContext = await m_CommandExecutor.ExecuteAsync(dummy, Arguments, string.Empty);
48 | commandContext.Exception =
49 | commandContext.Exception != null && m_ExceptionHandled ? null : commandContext.Exception;
50 | iEvent.Dispose();
51 |
52 | m_CommandExecutedHandler?.Invoke(commandContext);
53 | });
54 |
55 | return Task.CompletedTask;
56 | }
57 |
58 | [EventListener(Priority = EventListenerPriority.Monitor)]
59 | private Task OnCommandExecutedEvent(IServiceProvider serviceprovider, object? sender,
60 | CommandExecutedEvent @event)
61 | {
62 | if (Equals(@event.Actor, m_User))
63 | {
64 | m_ExceptionHandled = @event.ExceptionHandled;
65 | }
66 |
67 | return Task.CompletedTask;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/FaceAction.cs:
--------------------------------------------------------------------------------
1 | using Cysharp.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 | using SDG.Unturned;
5 | using System;
6 | using System.Threading.Tasks;
7 |
8 | namespace Dummy.Actions.Interaction.Actions
9 | {
10 | public class FaceAction : IAction
11 | {
12 | public FaceAction(byte index)
13 | {
14 | // 32 index face is an empty face (very scary)
15 | if (index > Customization.FACES_FREE + Customization.FACES_PRO)
16 | {
17 | throw new ArgumentOutOfRangeException(nameof(index));
18 | }
19 | Index = index;
20 | }
21 |
22 | public byte Index { get; }
23 |
24 | public Task Do(DummyUser dummy)
25 | {
26 | async UniTask SetFace()
27 | {
28 | await UniTask.SwitchToMainThread();
29 | // todo: bypass Nelson index checking
30 | dummy.Player.Player.clothing.ReceiveSwapFaceRequest(Index);
31 | }
32 |
33 | return SetFace().AsTask();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/GestureAction.cs:
--------------------------------------------------------------------------------
1 | using Cysharp.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 | using SDG.Unturned;
5 | using System.Threading.Tasks;
6 |
7 | namespace Dummy.Actions.Interaction.Actions
8 | {
9 | public class GestureAction : IAction
10 | {
11 | public GestureAction(EPlayerGesture ePlayerGesture)
12 | {
13 | Gesture = ePlayerGesture;
14 | }
15 |
16 | public EPlayerGesture Gesture { get; }
17 |
18 | public Task Do(DummyUser dummy)
19 | {
20 | async UniTask SetGesture()
21 | {
22 | await UniTask.SwitchToMainThread();
23 | dummy.Player.Player.animator.ReceiveGestureRequest(Gesture);
24 | }
25 |
26 | return SetGesture().AsTask();
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/MouseAction.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Cysharp.Threading.Tasks;
3 | using Dummy.API;
4 | using Dummy.Users;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using System.Threading.Tasks;
7 |
8 | namespace Dummy.Actions.Interaction.Actions
9 | {
10 | [UsedImplicitly]
11 | public class MouseAction : IAction
12 | {
13 | public MouseAction(MouseState state)
14 | {
15 | State = state;
16 | }
17 |
18 | public MouseState State { get; }
19 |
20 | public Task Do(DummyUser dummy)
21 | {
22 | dummy.Simulation.MouseState = State;
23 | return Task.CompletedTask;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/UI/ClickButtonAction.cs:
--------------------------------------------------------------------------------
1 | using Cysharp.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 | using SDG.Unturned;
5 | using System.Threading.Tasks;
6 | using Dummy.Extensions;
7 |
8 | namespace Dummy.Actions.Interaction.Actions.UI
9 | {
10 | public class ClickButtonAction : IAction
11 | {
12 | public ClickButtonAction(string buttonName)
13 | {
14 | ButtonName = buttonName;
15 | }
16 |
17 | public string ButtonName { get; }
18 |
19 | public Task Do(DummyUser dummy)
20 | {
21 | async UniTask EffectButtonClicked()
22 | {
23 | await UniTask.SwitchToMainThread();
24 | EffectManager.onEffectButtonClicked(dummy.Player.Player, ButtonName);
25 | }
26 | return EffectButtonClicked().AsTask();
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/UI/InputTextAction.cs:
--------------------------------------------------------------------------------
1 | using Cysharp.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 | using SDG.Unturned;
5 | using System.Threading.Tasks;
6 | using Dummy.Extensions;
7 |
8 | namespace Dummy.Actions.Interaction.Actions.UI
9 | {
10 | public class InputTextAction : IAction
11 | {
12 | public InputTextAction(string inputFieldName, string inputtedText)
13 | {
14 | InputtedText = inputtedText;
15 | InputFieldName = inputFieldName;
16 | }
17 |
18 | public string InputtedText { get; }
19 | public string InputFieldName { get; }
20 |
21 | public Task Do(DummyUser dummy)
22 | {
23 | async UniTask EffectTextCommitted()
24 | {
25 | await UniTask.SwitchToMainThread();
26 | EffectManager.onEffectTextCommitted(dummy.Player.Player, InputFieldName, InputtedText);
27 | }
28 | return EffectTextCommitted().AsTask();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/Vehicle/EnterVehicleAction.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Cysharp.Threading.Tasks;
3 | using Dummy.API;
4 | using Dummy.Users;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using SDG.Unturned;
7 | using System;
8 | using System.Threading.Tasks;
9 | using Dummy.Extensions;
10 |
11 | namespace Dummy.Actions.Interaction.Actions.Vehicle
12 | {
13 | [UsedImplicitly]
14 | public class EnterVehicleAction : IAction
15 | {
16 | public InteractableVehicle InteractableVehicle { get; }
17 |
18 | public EnterVehicleAction(InteractableVehicle interactableVehicle)
19 | {
20 | InteractableVehicle = interactableVehicle;
21 | }
22 |
23 | public EnterVehicleAction(uint instanceId)
24 | {
25 | InteractableVehicle = VehicleManager.findVehicleByNetInstanceID(instanceId);
26 | }
27 |
28 | public Task Do(DummyUser dummy)
29 | {
30 | if (InteractableVehicle is null)
31 | {
32 | throw new NullReferenceException(nameof(InteractableVehicle));
33 | }
34 |
35 | async UniTask ForceEnter()
36 | {
37 | await UniTask.SwitchToMainThread();
38 | VehicleManager.ServerForcePassengerIntoVehicle(dummy.Player.Player, InteractableVehicle);
39 | }
40 |
41 | return ForceEnter().AsTask();
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/Actions/Vehicle/ExitVehicleAction.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Cysharp.Threading.Tasks;
3 | using Dummy.API;
4 | using Dummy.Users;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using SDG.Unturned;
7 | using System.Threading.Tasks;
8 | using Dummy.Extensions;
9 | using UnityEngine;
10 |
11 | namespace Dummy.Actions.Interaction.Actions.Vehicle
12 | {
13 | [UsedImplicitly]
14 | public class ExitVehicleAction : IAction
15 | {
16 | public Task Do(DummyUser dummy)
17 | {
18 | async UniTask ExitVehicle()
19 | {
20 | await UniTask.SwitchToMainThread();
21 | VehicleManager.forceRemovePlayer(dummy.Player.SteamId);
22 | }
23 |
24 | return ExitVehicle().AsTask();
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Interaction/MouseState.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dummy.Actions.Interaction
4 | {
5 | [Flags]
6 | public enum MouseState
7 | {
8 | None = 0,
9 | Left = 1,
10 | Right = 1 << 1,
11 | LeftRight = Left | Right
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Dummy/Actions/JumpingEx.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Threading.Tasks;
4 | using Dummy.Actions.Movement.Actions;
5 | using Dummy.Users;
6 | using JetBrainsAnnotations::JetBrains.Annotations;
7 |
8 | namespace Dummy.Actions
9 | {
10 | [UsedImplicitly(ImplicitUseTargetFlags.Members)]
11 | public static class JumpingEx
12 | {
13 | public static void JumpingConstantOn(this DummyUser dummy)
14 | {
15 | dummy.Actions.ContinuousActions.Add(new JumpAction(true));
16 | }
17 |
18 | public static void JumpingConstantOff(this DummyUser dummy)
19 | {
20 | dummy.Actions.ContinuousActions.RemoveAll(c => c is JumpAction);
21 | }
22 |
23 | public static void Jump(this DummyUser dummy)
24 | {
25 | dummy.Actions.Actions.Enqueue(new JumpAction(true));
26 | dummy.Actions.Actions.Enqueue(new JumpAction(false));
27 | }
28 |
29 | public static async Task TempJumpAsync(this DummyUser playerDummy, float time)
30 | {
31 | playerDummy.JumpingConstantOn();
32 | await Task.Delay(TimeSpan.FromSeconds(time));
33 | playerDummy.JumpingConstantOff();
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/Dummy/Actions/MouseEx.cs:
--------------------------------------------------------------------------------
1 | using Dummy.Actions.Interaction;
2 | using Dummy.Actions.Interaction.Actions;
3 | using Dummy.Users;
4 |
5 | namespace Dummy.Actions;
6 | public static class MouseEx
7 | {
8 | public static void Punch(this DummyUser dummy, MouseState state)
9 | {
10 | dummy.Actions.Actions.Enqueue(new MouseAction(state));
11 | dummy.Actions.Actions.Enqueue(new MouseAction(MouseState.None));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Dummy/Actions/Movement/Actions/JumpAction.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 |
5 | namespace Dummy.Actions.Movement.Actions
6 | {
7 | public class JumpAction : IAction
8 | {
9 | public bool IsJumping { get; }
10 |
11 | public JumpAction(bool isJumping)
12 | {
13 | IsJumping = isJumping;
14 | }
15 |
16 | public Task Do(DummyUser dummy)
17 | {
18 | dummy.Simulation.Jump = IsJumping;
19 | return Task.CompletedTask;
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Movement/Actions/LookAtAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.API;
5 | using Dummy.Users;
6 | using OpenMod.UnityEngine.Extensions;
7 | using UnityEngine;
8 |
9 | namespace Dummy.Actions.Movement.Actions
10 | {
11 | public class LookAtAction : IAction
12 | {
13 | private readonly Vector3 m_Pos;
14 |
15 | public LookAtAction(Vector3 pos)
16 | {
17 | m_Pos = pos;
18 | }
19 |
20 | public Task Do(DummyUser dummy)
21 | {
22 | async UniTask Task()
23 | {
24 | await UniTask.SwitchToMainThread();
25 | Vector3 dir = (m_Pos - dummy.Player.Transform.Position.ToUnityVector()).normalized;
26 | float yaw = (float)Math.Atan2(dir.x, dir.y);
27 | float pitch = (float)Math.Atan2(dir.z, Math.Sqrt((dir.x * dir.x) + (dir.y * dir.y)));
28 | RotateAction rotation = new(yaw, pitch);
29 | await rotation.Do(dummy);
30 | }
31 |
32 | return Task().AsTask();
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Movement/Actions/RotateAction.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 |
5 | namespace Dummy.Actions.Movement.Actions
6 | {
7 | public class RotateAction : IAction
8 | {
9 | public RotateAction(float yaw, float pitch)
10 | {
11 | Yaw = yaw;
12 | Pitch = pitch;
13 | }
14 |
15 | public float Yaw { get; }
16 | public float Pitch { get; }
17 |
18 | public Task Do(DummyUser dummy)
19 | {
20 | dummy.Simulation.SetRotation(Yaw, Pitch, 0);
21 | return Task.CompletedTask;
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Movement/Actions/SprintAction.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System.Threading.Tasks;
3 | using Dummy.API;
4 | using Dummy.Users;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 |
7 | namespace Dummy.Actions.Movement.Actions
8 | {
9 | [UsedImplicitly]
10 | public class SprintAction : IAction
11 | {
12 | public bool IsSprinting { get; }
13 |
14 | public SprintAction(bool isSprinting)
15 | {
16 | IsSprinting = isSprinting;
17 | }
18 |
19 | public Task Do(DummyUser dummy)
20 | {
21 | dummy.Simulation.Sprint = IsSprinting;
22 | return Task.CompletedTask;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Movement/Actions/StanceAction.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Dummy.API;
3 | using Dummy.Users;
4 | using SDG.Unturned;
5 |
6 | namespace Dummy.Actions.Movement.Actions
7 | {
8 | public class StanceAction : IAction
9 | {
10 | public StanceAction(bool wantsCrouch, bool wantsProne)
11 | {
12 | WantsCrouch = wantsCrouch;
13 | WantsProne = wantsProne;
14 | }
15 |
16 | public StanceAction(EPlayerStance stance)
17 | {
18 | WantsCrouch = stance == EPlayerStance.CROUCH;
19 | WantsProne = stance == EPlayerStance.PRONE;
20 | }
21 |
22 | public bool WantsCrouch { get; }
23 | public bool WantsProne { get; }
24 |
25 | public Task Do(DummyUser dummy)
26 | {
27 | dummy.Simulation.Crouch = WantsCrouch;
28 | dummy.Simulation.Prone = WantsProne;
29 |
30 | return Task.CompletedTask;
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Movement/Actions/StrafeAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Dummy.API;
4 | using Dummy.Users;
5 | using UnityEngine;
6 |
7 | namespace Dummy.Actions.Movement.Actions
8 | {
9 | public class StrafeAction : IAction
10 | {
11 | private Vector3? m_Vector;
12 |
13 | public StrafeDirection Direction { get; }
14 |
15 | public StrafeAction(StrafeDirection direction)
16 | {
17 | Direction = direction;
18 | }
19 |
20 | public StrafeAction(int x, int y) : this(new Vector2(x, y))
21 | {
22 | }
23 |
24 | public StrafeAction(Vector2 vector)
25 | {
26 | m_Vector = new(vector.x, 0, vector.y);
27 | }
28 |
29 | public Task Do(DummyUser dummy)
30 | {
31 | dummy.Simulation.Move = m_Vector ?? Direction switch
32 | {
33 | StrafeDirection.None => new(0, 0, 0),
34 | StrafeDirection.Left => new(-1, 0, 0),
35 | StrafeDirection.Right => new(1, 0, 0),
36 | StrafeDirection.Forward => new(0, 0, 1),
37 | StrafeDirection.Backward => new(0, 0, -1),
38 | StrafeDirection.ForwardLeft => new(-1, 0, 1),
39 | StrafeDirection.ForwardRight => new(1, 0, 1),
40 | StrafeDirection.BackwardLeft => new(-1, 0, -1),
41 | StrafeDirection.BackwardRight => new(1, 0, -1),
42 | _ => throw new ArgumentOutOfRangeException(nameof(Direction), Direction, "Tried to strafe to wrong direction")
43 | };
44 | return Task.CompletedTask;
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/Dummy/Actions/StrafeDirection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dummy.Actions
4 | {
5 | [Flags]
6 | public enum StrafeDirection
7 | {
8 | None = 0,
9 | Left = 1,
10 | Right = 1 << 1,
11 | Forward = 1 << 2,
12 | Backward = 1 << 3,
13 | ForwardLeft = Forward | Left,
14 | ForwardRight = Forward | Right,
15 | BackwardLeft = Backward | Left,
16 | BackwardRight = Backward | Right
17 | }
18 | }
--------------------------------------------------------------------------------
/Dummy/Actions/Strafing.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Dummy.Actions.Movement.Actions;
3 | using Dummy.Users;
4 | using JetBrainsAnnotations::JetBrains.Annotations;
5 |
6 | namespace Dummy.Actions
7 | {
8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)]
9 | public static class Strafing
10 | {
11 | public static void WalkingConstantOn(this DummyUser dummy, StrafeDirection direction)
12 | {
13 | dummy.Actions.ContinuousActions.Add(new StrafeAction(direction));
14 | }
15 |
16 | public static void WalkingConstantOff(this DummyUser dummy, StrafeDirection direction)
17 | {
18 | dummy.Actions.ContinuousActions.RemoveAll(c =>
19 | c is StrafeAction strafeAction && strafeAction.Direction == direction);
20 | }
21 |
22 | public static void Walk(this DummyUser dummy, StrafeDirection direction)
23 | {
24 | Sprint(dummy, direction, false);
25 | }
26 |
27 | public static void Sprint(this DummyUser dummyUser, StrafeDirection direction, bool isSprinting)
28 | {
29 | dummyUser.Actions.Actions.Enqueue(new StrafeAction(direction));
30 | dummyUser.Actions.Actions.Enqueue(new SprintAction(isSprinting));
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/Dummy/Actions/SwappingFace.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Dummy.Actions.Interaction.Actions;
3 | using Dummy.Users;
4 | using JetBrainsAnnotations::JetBrains.Annotations;
5 |
6 | namespace Dummy.Actions
7 | {
8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)]
9 | public static class SwappingFace
10 | {
11 | public static void SwapFace(this DummyUser dummy, byte index)
12 | {
13 | dummy.Actions.Actions.Enqueue(new FaceAction(index));
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyButton.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions.Interaction.Actions.UI;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 |
11 | namespace Dummy.Commands.Actions
12 | {
13 | [Command("button")]
14 | [CommandSyntax(" ")]
15 | [CommandParent(typeof(CommandDummy))]
16 | [UsedImplicitly]
17 | public class CommandDummyButton : CommandDummyAction
18 | {
19 | private readonly IStringLocalizer m_StringLocalizer;
20 |
21 | public CommandDummyButton(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
22 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
23 | {
24 | m_StringLocalizer = stringLocalizer;
25 | }
26 |
27 | protected override UniTask ExecuteDummyAsync(DummyUser playerDummy)
28 | {
29 | if (Context.Parameters.Count != 2)
30 | {
31 | throw new CommandWrongUsageException(Context);
32 | }
33 |
34 | var buttonName = Context.Parameters[1];
35 | playerDummy.Actions.Actions.Enqueue(new ClickButtonAction(buttonName));
36 | return PrintAsync(m_StringLocalizer["commands:actions:button:success", new { ButtonName = buttonName }])
37 | .AsUniTask();
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyExecute.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Linq;
4 | using Cysharp.Threading.Tasks;
5 | using Dummy.Actions.Interaction.Actions;
6 | using Dummy.API;
7 | using Dummy.Users;
8 | using JetBrainsAnnotations::JetBrains.Annotations;
9 | using Microsoft.Extensions.Localization;
10 | using OpenMod.API.Commands;
11 | using OpenMod.API.Eventing;
12 | using OpenMod.Core.Commands;
13 |
14 | namespace Dummy.Commands.Actions
15 | {
16 | [Command("execute")]
17 | [CommandDescription("Execute a command by Dummy")]
18 | [CommandSyntax(" ")]
19 | [CommandParent(typeof(CommandDummy))]
20 | [UsedImplicitly]
21 | public class CommandDummyExecute : CommandDummyAction
22 | {
23 | private readonly ICommandExecutor m_CommandExecutor;
24 | private readonly IStringLocalizer m_StringLocalizer;
25 | private readonly IEventBus m_EventBus;
26 | private readonly Dummy m_Plugin;
27 |
28 | public CommandDummyExecute(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
29 | ICommandExecutor commandExecutor, IStringLocalizer stringLocalizer, IEventBus eventBus, Dummy plugin) :
30 | base(serviceProvider, dummyProvider, stringLocalizer)
31 | {
32 | m_CommandExecutor = commandExecutor;
33 | m_StringLocalizer = stringLocalizer;
34 | m_EventBus = eventBus;
35 | m_Plugin = plugin;
36 | }
37 |
38 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
39 | {
40 | if (Context.Parameters.Count < 2)
41 | {
42 | throw new CommandWrongUsageException(Context);
43 | }
44 |
45 | var wait = false;
46 | Exception? exception = null;
47 | var command = Context.Parameters.Skip(1).ToArray();
48 |
49 | playerDummy.Actions.Actions.Enqueue(new ExecuteCommandAction(m_CommandExecutor, command, m_Plugin,
50 | m_EventBus, context =>
51 | {
52 | wait = true;
53 | exception = context.Exception;
54 | }));
55 | await UniTask.WaitUntil(() => wait);
56 |
57 | if (exception != null)
58 | {
59 | await PrintAsync(m_StringLocalizer["commands:actions:execute:fail",
60 | new { playerDummy.Id, Command = string.Join(" ", command) }]);
61 | }
62 | else
63 | {
64 | await PrintAsync(m_StringLocalizer["commands:actions:execute:success",
65 | new { playerDummy.Id, Command = string.Join(" ", command) }]);
66 | }
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyFace.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions.Interaction.Actions;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 |
11 | namespace Dummy.Commands.Actions
12 | {
13 | [Command("face")]
14 | [CommandDescription("Send a face to dummy")]
15 | [CommandParent(typeof(CommandDummy))]
16 | [CommandSyntax(" ")]
17 | [UsedImplicitly]
18 | public class CommandDummyFace : CommandDummyAction
19 | {
20 | private readonly IStringLocalizer m_StringLocalizer;
21 |
22 | public CommandDummyFace(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
23 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
24 | {
25 | m_StringLocalizer = stringLocalizer;
26 | }
27 |
28 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
29 | {
30 | if (Context.Parameters.Count != 2)
31 | {
32 | throw new CommandWrongUsageException(Context);
33 | }
34 |
35 | var faceId = await Context.Parameters.GetAsync(1);
36 | playerDummy.Actions.Actions.Enqueue(new FaceAction(faceId));
37 | await PrintAsync(m_StringLocalizer["commands:actions:face:success",
38 | new { playerDummy.Id, FaceIndex = faceId }]);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyGesture.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions.Interaction.Actions;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 | using SDG.Unturned;
11 |
12 | namespace Dummy.Commands.Actions
13 | {
14 | [Command("gesture")]
15 | [CommandDescription("Make a dummy gesture")]
16 | [CommandParent(typeof(CommandDummy))]
17 | [CommandSyntax(" ")]
18 | [UsedImplicitly]
19 | public class CommandDummyGesture : CommandDummyAction
20 | {
21 | private readonly IStringLocalizer m_StringLocalizer;
22 |
23 | public CommandDummyGesture(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
24 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
25 | {
26 | m_StringLocalizer = stringLocalizer;
27 | }
28 |
29 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
30 | {
31 | if (Context.Parameters.Count != 2)
32 | {
33 | throw new CommandWrongUsageException(Context);
34 | }
35 |
36 | var gesture = Context.Parameters[1];
37 | if (!Enum.TryParse(gesture, true, out var eGesture))
38 | {
39 | await PrintAsync($"Unable find a gesture {gesture}");
40 | await PrintAsync($"All gestures: {string.Join(", ", Enum.GetNames(typeof(EPlayerGesture)))}");
41 | return;
42 | }
43 |
44 | playerDummy.Actions.Actions.Enqueue(new GestureAction(eGesture));
45 | await PrintAsync(m_StringLocalizer["commands:actions:gesture:success",
46 | new { playerDummy.Id, EGesture = eGesture }]);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyInputText.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions.Interaction.Actions.UI;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 |
11 | namespace Dummy.Commands.Actions
12 | {
13 | [Command("inputfield")]
14 | [CommandAlias("if")]
15 | [CommandSyntax(" ")]
16 | [CommandParent(typeof(CommandDummy))]
17 | [UsedImplicitly]
18 | public class CommandDummyInputText : CommandDummyAction
19 | {
20 | private readonly IStringLocalizer m_StringLocalizer;
21 |
22 | public CommandDummyInputText(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
23 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
24 | {
25 | m_StringLocalizer = stringLocalizer;
26 | }
27 |
28 | protected override UniTask ExecuteDummyAsync(DummyUser playerDummy)
29 | {
30 | if (Context.Parameters.Count is < 2)
31 | {
32 | throw new CommandWrongUsageException(Context);
33 | }
34 |
35 | var inputFieldName = Context.Parameters[1];
36 | var text = Context.Parameters.GetArgumentLine(2);
37 | playerDummy.Actions.Actions.Enqueue(new InputTextAction(inputFieldName, text));
38 | return PrintAsync(m_StringLocalizer["commands:actions:inputfield:success",
39 | new { playerDummy.Id, Text = text, InputFieldName = inputFieldName }]).AsUniTask();
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyJump.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 |
11 | namespace Dummy.Commands.Actions
12 | {
13 | [Command("jump")]
14 | [CommandDescription("Make dummy to jump")]
15 | [CommandSyntax("")]
16 | [CommandParent(typeof(CommandDummy))]
17 | [UsedImplicitly]
18 | public class CommandDummyJump : CommandDummyAction
19 | {
20 | public CommandDummyJump(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
21 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
22 | {
23 | }
24 |
25 | protected override UniTask ExecuteDummyAsync(DummyUser playerDummy)
26 | {
27 | playerDummy.Jump();
28 | return UniTask.CompletedTask;
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyLook.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions.Movement.Actions;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 |
11 | namespace Dummy.Commands.Actions
12 | {
13 | [Command("look")]
14 | [CommandParent(typeof(CommandDummy))]
15 | [CommandSyntax(" ")]
16 | [UsedImplicitly]
17 | public class CommandDummyLook : CommandDummyAction
18 | {
19 | public CommandDummyLook(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
20 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
21 | {
22 | }
23 |
24 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
25 | {
26 | if (Context.Parameters.Count != 3)
27 | {
28 | throw new CommandWrongUsageException(Context);
29 | }
30 |
31 | var yaw = await Context.Parameters.GetAsync(1);
32 | var pitch = await Context.Parameters.GetAsync(2);
33 |
34 | playerDummy.Actions.Actions.Enqueue(new RotateAction(yaw, pitch));
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyMouse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Cysharp.Threading.Tasks;
3 | using Dummy.Actions;
4 | using Dummy.Actions.Interaction;
5 | using Dummy.Actions.Interaction.Actions;
6 | using Dummy.API;
7 | using Dummy.Users;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 | using SDG.Unturned;
11 |
12 | namespace Dummy.Commands.Actions;
13 | [Command("mouse")]
14 | [CommandParent(typeof(CommandDummy))]
15 | [CommandSyntax(" ")]
16 | public class CommandDummyMouse : CommandDummyAction
17 | {
18 | public CommandDummyMouse(IServiceProvider serviceProvider, IDummyProvider dummyProvider, IStringLocalizer stringLocalizer)
19 | : base(serviceProvider, dummyProvider, stringLocalizer)
20 | {
21 | }
22 |
23 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
24 | {
25 | if (Context.Parameters.Count != 2)
26 | {
27 | throw new CommandWrongUsageException(Context);
28 | }
29 |
30 | if (!Enum.TryParse(Context.Parameters[1], true, out var state))
31 | {
32 | await PrintAsync("Unable parse a mouse state");
33 | await PrintAsync($"All mouse states: {string.Join(", ", Enum.GetNames(typeof(MouseState)))}");
34 | return;
35 | }
36 |
37 | playerDummy.Punch(state);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyMove.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Cysharp.Threading.Tasks;
3 | using Dummy.Actions;
4 | using Dummy.Actions.Movement.Actions;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using Microsoft.Extensions.Localization;
8 | using OpenMod.Core.Commands;
9 | using UnityEngine;
10 |
11 | namespace Dummy.Commands.Actions
12 | {
13 | [Command("move")]
14 | [CommandParent(typeof(CommandDummy))]
15 | [CommandSyntax(" ")]
16 | public class CommandDummyMove : CommandDummyAction
17 | {
18 | public CommandDummyMove(IServiceProvider serviceProvider, IDummyProvider dummyProvider, IStringLocalizer stringLocalizer)
19 | : base(serviceProvider, dummyProvider, stringLocalizer)
20 | {
21 | }
22 |
23 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
24 | {
25 | if (Context.Parameters.Count <= 1)
26 | {
27 | throw new CommandWrongUsageException(Context);
28 | }
29 |
30 | StrafeAction? strafeAction;
31 |
32 | if (Context.Parameters.Count == 2)
33 | {
34 | var strafe = Context.Parameters[1];
35 |
36 | if (!Enum.TryParse(strafe, true, out var dir))
37 | {
38 | await PrintAsync($"Unable find a strafe direction {strafe}");
39 | await PrintAsync($"All strafes: {string.Join(", ", Enum.GetNames(typeof(StrafeDirection)))}");
40 | return;
41 | }
42 |
43 | strafeAction = new(dir);
44 | }
45 | else if (Context.Parameters.Count == 3)
46 | {
47 | var strafeX = await Context.Parameters.GetAsync(1);
48 | var strafeY = await Context.Parameters.GetAsync(2);
49 |
50 | strafeX = Mathf.Clamp(strafeX, -1, 1);
51 | strafeY = Mathf.Clamp(strafeY, -1, 1);
52 |
53 | strafeAction = new(strafeX, strafeY);
54 | }
55 | else
56 | {
57 | throw new CommandWrongUsageException(Context);
58 | }
59 |
60 | playerDummy.Actions.Actions.Enqueue(strafeAction);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyRide.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Cysharp.Threading.Tasks;
3 | using Dummy.Actions.Interaction.Actions.Vehicle;
4 | using Dummy.API;
5 | using Dummy.Users;
6 | using Microsoft.Extensions.Localization;
7 | using OpenMod.API.Commands;
8 | using OpenMod.Core.Commands;
9 | using OpenMod.Extensions.Games.Abstractions.Players;
10 | using OpenMod.Extensions.Games.Abstractions.Vehicles;
11 | using OpenMod.Unturned.Users;
12 | using OpenMod.Unturned.Vehicles;
13 | using SDG.Framework.Utilities;
14 | using SDG.Unturned;
15 | using UnityEngine;
16 |
17 | namespace Dummy.Commands.Actions
18 | {
19 | [Command("ride")]
20 | [CommandParent(typeof(CommandDummy))]
21 | [CommandSyntax(" [--exit]")]
22 | public class CommandDummyRide : CommandDummyAction
23 | {
24 | public CommandDummyRide(IServiceProvider serviceProvider, IDummyProvider dummyProvider, IStringLocalizer stringLocalizer)
25 | : base(serviceProvider, dummyProvider, stringLocalizer)
26 | {
27 | }
28 |
29 | protected override UniTask ExecuteDummyAsync(DummyUser playerDummy)
30 | {
31 | var exitText = Context.Parameters.Count >= 2 ? Context.Parameters[1].ToLower() : null;
32 | var isExit = exitText switch
33 | {
34 | "exit" or "e" or "--exit" or "-e" => true,
35 | _ => false
36 | };
37 |
38 | if (isExit)
39 | {
40 | playerDummy.Actions.Actions.Enqueue(new ExitVehicleAction());
41 | return UniTask.CompletedTask;
42 | }
43 |
44 | InteractableVehicle? vehicle1 = null;
45 |
46 | if (Context.Actor is IPlayerUser playerUser)
47 | {
48 | if (playerUser.Player is ICanEnterVehicle vehicleActor && vehicleActor.CurrentVehicle is UnturnedVehicle vehicle)
49 | {
50 | vehicle1 = vehicle.Vehicle;
51 | }
52 |
53 | if (vehicle1 == null && playerUser is UnturnedUser unturnedUser)
54 | {
55 | var aim = unturnedUser.Player.Player.look.aim;
56 | Physics.Raycast(new(aim.position, aim.forward), out var hit, 8f, RayMasks.VEHICLE);
57 | if (hit.transform != null)
58 | {
59 | vehicle1 = DamageTool.getVehicle(hit.transform);
60 | }
61 | }
62 | }
63 |
64 | if (vehicle1 == null)
65 | {
66 | throw new UserFriendlyException("You're not in a vehicle or not look at it");
67 | }
68 |
69 | playerDummy.Actions.Actions.Enqueue(new EnterVehicleAction(vehicle1));
70 |
71 | return UniTask.CompletedTask;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Dummy/Commands/Actions/CommandDummyStance.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.Actions.Movement.Actions;
5 | using Dummy.API;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 | using SDG.Unturned;
11 |
12 | namespace Dummy.Commands.Actions
13 | {
14 | [Command("stance")]
15 | [CommandSyntax(" ")]
16 | [CommandParent(typeof(CommandDummy))]
17 | [UsedImplicitly]
18 | public class CommandDummyStance : CommandDummyAction
19 | {
20 | private readonly IStringLocalizer m_StringLocalizer;
21 |
22 | public CommandDummyStance(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
23 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
24 | {
25 | m_StringLocalizer = stringLocalizer;
26 | }
27 |
28 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
29 | {
30 | if (Context.Parameters.Count != 2)
31 | {
32 | throw new CommandWrongUsageException(Context);
33 | }
34 |
35 | var stance = Context.Parameters[1];
36 | if (!Enum.TryParse(stance, true, out var eStance))
37 | {
38 | await PrintAsync($"Unable to find a stance: {stance}");
39 | await PrintAsync($"All stances: {string.Join(", ", Enum.GetNames(typeof(EPlayerStance)))}");
40 | return;
41 | }
42 |
43 | playerDummy.Actions.Actions.Enqueue(new StanceAction(eStance));
44 | await PrintAsync(m_StringLocalizer["commands:actions:stance:success",
45 | new { playerDummy.Id, EStance = eStance }]);
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummy.cs:
--------------------------------------------------------------------------------
1 | using Dummy.Commands.Helpers;
2 | using OpenMod.Core.Commands;
3 | using System;
4 | using System.Threading.Tasks;
5 | using Command = OpenMod.Core.Commands.Command;
6 |
7 | namespace Dummy.Commands
8 | {
9 | [Command("dummy")]
10 | // ReSharper disable StringLiteralTypo
11 | [CommandSyntax("")]
12 | public class CommandDummy : Command
13 | {
14 | internal static readonly CommandArgument s_NameArgument = new("--name");
15 |
16 | public CommandDummy(IServiceProvider serviceProvider) : base(serviceProvider)
17 | {
18 | }
19 |
20 | protected override Task OnExecuteAsync()
21 | {
22 | return Task.FromException(new CommandWrongUsageException(Context));
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummyAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.API;
5 | using Dummy.Users;
6 | using Microsoft.Extensions.Localization;
7 | using OpenMod.API.Commands;
8 | using OpenMod.Core.Commands;
9 | using OpenMod.Core.Ioc;
10 | using Steamworks;
11 |
12 | namespace Dummy.Commands
13 | {
14 | [DontAutoRegister]
15 | public abstract class CommandDummyAction : Command
16 | {
17 | private readonly IDummyProvider m_DummyProvider;
18 | private readonly IStringLocalizer m_StringLocalizer;
19 |
20 | protected CommandDummyAction(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
21 | IStringLocalizer stringLocalizer) : base(serviceProvider)
22 | {
23 | m_DummyProvider = dummyProvider;
24 | m_StringLocalizer = stringLocalizer;
25 | }
26 |
27 | protected override async Task OnExecuteAsync()
28 | {
29 | if (Context.Parameters.Count == 0)
30 | {
31 | throw new CommandWrongUsageException(Context);
32 | }
33 |
34 | if (Context.Parameters[0].Equals("all", StringComparison.OrdinalIgnoreCase))
35 | {
36 | foreach (var dummyUser in m_DummyProvider.Dummies)
37 | {
38 | await ExecuteDummyAsync(dummyUser);
39 | }
40 | return;
41 | }
42 |
43 | var id = await Context.Parameters.GetAsync(0);
44 |
45 | var dummy = await m_DummyProvider.FindDummyUserAsync(id)
46 | ?? throw new UserFriendlyException(m_StringLocalizer["commands:dummyNotFound", new { Id = id }]);
47 | await ExecuteDummyAsync(dummy);
48 | }
49 |
50 | protected abstract UniTask ExecuteDummyAsync(DummyUser playerDummy);
51 | }
52 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummyClear.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Threading.Tasks;
4 | using Dummy.API;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using Microsoft.Extensions.Localization;
7 | using OpenMod.Core.Commands;
8 |
9 | namespace Dummy.Commands
10 | {
11 | [Command("clear")]
12 | [CommandDescription("Clears all dummies")]
13 | [CommandParent(typeof(CommandDummy))]
14 | [UsedImplicitly]
15 | public class CommandDummyClear : Command
16 | {
17 | private readonly IDummyProvider m_DummyProvider;
18 | private readonly IStringLocalizer m_StringLocalizer;
19 |
20 | public CommandDummyClear(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
21 | IStringLocalizer stringLocalizer) : base(serviceProvider)
22 | {
23 | m_DummyProvider = dummyProvider;
24 | m_StringLocalizer = stringLocalizer;
25 | }
26 |
27 | protected override async Task OnExecuteAsync()
28 | {
29 | await m_DummyProvider.ClearDummiesAsync();
30 | await PrintAsync(m_StringLocalizer["commands:general:clear"]);
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummyCopy.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Threading.Tasks;
4 | using Dummy.API;
5 | using Dummy.Extensions;
6 | using Dummy.Models;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Localization;
10 | using OpenMod.Core.Commands;
11 | using OpenMod.Unturned.Users;
12 |
13 | namespace Dummy.Commands
14 | {
15 | [Command("copy")]
16 | [CommandDescription("Creates a dummy and copy your skin, hair, beard, etc...")]
17 | [CommandActor(typeof(UnturnedUser))]
18 | [CommandParent(typeof(CommandDummy))]
19 | [UsedImplicitly]
20 | public class CommandDummyCopy : Command
21 | {
22 | private readonly IDummyProvider m_DummyProvider;
23 | private readonly IStringLocalizer m_StringLocalizer;
24 | private readonly IConfiguration m_Configuration;
25 |
26 | public CommandDummyCopy(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
27 | IStringLocalizer stringLocalizer, IConfiguration configuration) : base(serviceProvider)
28 | {
29 | m_DummyProvider = dummyProvider;
30 | m_StringLocalizer = stringLocalizer;
31 | m_Configuration = configuration;
32 | }
33 |
34 | protected override async Task OnExecuteAsync()
35 | {
36 | var user = (UnturnedUser)Context.Actor;
37 |
38 | ConfigurationSettings? settings = null;
39 | var name = CommandDummy.s_NameArgument.GetArgument(Context.Parameters);
40 | if (!string.IsNullOrEmpty(name))
41 | {
42 | settings = m_Configuration.GetSection("default").Get();
43 |
44 | settings!.CharacterName = name!;
45 | settings.NickName = name!;
46 | settings.PlayerName = name!;
47 | }
48 |
49 | var playerDummy =
50 | await m_DummyProvider.AddDummyAsync(null, new() { user.SteamId }, user, settings);
51 |
52 | await playerDummy.TeleportToPlayerAsync(user);
53 |
54 | await PrintAsync(m_StringLocalizer["commands:general:copy", new { Id = playerDummy.SteamId }]);
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummyCreate.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Threading.Tasks;
4 | using Dummy.API;
5 | using Dummy.Extensions;
6 | using Dummy.Models;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Localization;
10 | using OpenMod.Core.Commands;
11 | using OpenMod.Unturned.Users;
12 |
13 | namespace Dummy.Commands
14 | {
15 | [Command("create")]
16 | [CommandDescription("Creates a dummy")]
17 | [CommandActor(typeof(UnturnedUser))]
18 | [CommandParent(typeof(CommandDummy))]
19 | [UsedImplicitly]
20 | public class CommandDummyCreate : Command
21 | {
22 | private readonly IDummyProvider m_DummyProvider;
23 | private readonly IStringLocalizer m_StringLocalizer;
24 | private readonly IConfiguration m_Configuration;
25 |
26 | public CommandDummyCreate(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
27 | IStringLocalizer stringLocalizer, IConfiguration configuration) : base(serviceProvider)
28 | {
29 | m_DummyProvider = dummyProvider;
30 | m_StringLocalizer = stringLocalizer;
31 | m_Configuration = configuration;
32 | }
33 |
34 | protected override async Task OnExecuteAsync()
35 | {
36 | var user = (UnturnedUser)Context.Actor;
37 |
38 | ConfigurationSettings? settings = null;
39 | var name = CommandDummy.s_NameArgument.GetArgument(Context.Parameters);
40 | if (!string.IsNullOrEmpty(name))
41 | {
42 | settings = m_Configuration.GetSection("default").Get();
43 |
44 | settings!.CharacterName = name!;
45 | settings.NickName = name!;
46 | settings.PlayerName = name!;
47 | }
48 |
49 | var playerDummy = await m_DummyProvider.AddDummyByParameters(null, new() { user.SteamId }, settings);
50 | await playerDummy.TeleportToPlayerAsync(user);
51 |
52 | await PrintAsync(m_StringLocalizer["commands:general:create", new { Id = playerDummy.SteamId }]);
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummyRemove.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Threading.Tasks;
4 | using Dummy.API;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using Microsoft.Extensions.Localization;
7 | using OpenMod.Core.Commands;
8 | using Steamworks;
9 |
10 | namespace Dummy.Commands
11 | {
12 | [Command("remove")]
13 | [CommandAlias("kick")]
14 | [CommandAlias("delete")]
15 | [CommandDescription("Removes a dummy by id")]
16 | [CommandParent(typeof(CommandDummy))]
17 | [CommandSyntax("")]
18 | [UsedImplicitly]
19 | public class CommandDummyRemove : Command
20 | {
21 | private readonly IDummyProvider m_DummyProvider;
22 | private readonly IStringLocalizer m_StringLocalizer;
23 |
24 | public CommandDummyRemove(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
25 | IStringLocalizer stringLocalizer) : base(serviceProvider)
26 | {
27 | m_DummyProvider = dummyProvider;
28 | m_StringLocalizer = stringLocalizer;
29 | }
30 |
31 | protected override async Task OnExecuteAsync()
32 | {
33 | if (Context.Parameters.Count == 0)
34 | {
35 | throw new CommandWrongUsageException(Context);
36 | }
37 |
38 | var id = (CSteamID)await Context.Parameters.GetAsync(0);
39 |
40 | if (await m_DummyProvider.RemoveDummyAsync(id))
41 | {
42 | await PrintAsync(m_StringLocalizer["commands:general:remove:success", new { Id = id }]);
43 | return;
44 | }
45 |
46 | await PrintAsync(m_StringLocalizer["commands:general:remove:fail", new { Id = id }]);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummySpawn.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 | using Dummy.API;
6 | using Dummy.Models;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Configuration;
9 | using OpenMod.Core.Commands;
10 | using Steamworks;
11 |
12 | namespace Dummy.Commands
13 | {
14 | [Command("spawn")]
15 | [CommandParent(typeof(CommandDummy))]
16 | [UsedImplicitly]
17 | public class CommandDummySpawn : Command
18 | {
19 | private readonly IDummyProvider m_DummyProvider;
20 | private readonly IConfiguration m_Configuration;
21 |
22 | public CommandDummySpawn(IServiceProvider serviceProvider, IDummyProvider dummyProvider, IConfiguration configuration) : base(serviceProvider)
23 | {
24 | m_DummyProvider = dummyProvider;
25 | m_Configuration = configuration;
26 | }
27 |
28 | protected override async Task OnExecuteAsync()
29 | {
30 | var settings = m_Configuration.Get()!.Default;
31 | var name = CommandDummy.s_NameArgument.GetArgument(Context.Parameters);
32 | if (!string.IsNullOrEmpty(name))
33 | {
34 | settings.CharacterName = name!;
35 | settings.NickName = name!;
36 | settings.PlayerName = name!;
37 | }
38 |
39 | await m_DummyProvider.AddDummyByParameters(null, new HashSet(), settings);
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Dummy/Commands/CommandDummyTphere.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.API;
5 | using Dummy.Extensions;
6 | using Dummy.Users;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using Microsoft.Extensions.Localization;
9 | using OpenMod.Core.Commands;
10 | using OpenMod.Unturned.Users;
11 |
12 | namespace Dummy.Commands
13 | {
14 | [Command("tphere")]
15 | [CommandDescription("Teleport to you a dummy")]
16 | [CommandActor(typeof(UnturnedUser))]
17 | [CommandSyntax("")]
18 | [CommandParent(typeof(CommandDummy))]
19 | [UsedImplicitly]
20 | public class CommandDummyTphere : CommandDummyAction
21 | {
22 | private readonly IStringLocalizer m_StringLocalizer;
23 |
24 | public CommandDummyTphere(IServiceProvider serviceProvider, IDummyProvider dummyProvider,
25 | IStringLocalizer stringLocalizer) : base(serviceProvider, dummyProvider, stringLocalizer)
26 | {
27 | m_StringLocalizer = stringLocalizer;
28 | }
29 |
30 | protected override async UniTask ExecuteDummyAsync(DummyUser playerDummy)
31 | {
32 | if (await playerDummy.TeleportToPlayerAsync((UnturnedUser)Context.Actor))
33 | {
34 | await PrintAsync(m_StringLocalizer["commands:general:tphere:success", new { playerDummy.Id }]);
35 | }
36 | else
37 | {
38 | await PrintAsync(m_StringLocalizer["commands:general:tphere:fail", new { playerDummy.Id }]);
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Dummy/Commands/Helpers/CommandArgument.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using OpenMod.API.Commands;
3 |
4 | namespace Dummy.Commands.Helpers;
5 | internal sealed class CommandArgument
6 | {
7 | private readonly string m_Name;
8 | private readonly string? m_ShortName;
9 |
10 | public CommandArgument(string name, char? shortName = null)
11 | {
12 | m_Name = name;
13 | if (!m_Name.StartsWith("--", StringComparison.OrdinalIgnoreCase))
14 | {
15 | m_Name = "--" + m_Name;
16 | }
17 |
18 | if (shortName != null)
19 | {
20 | m_ShortName = "-" + shortName;
21 | }
22 | }
23 |
24 | public string? GetArgument(ICommandParameters parameters)
25 | {
26 | for (var i = 0; i < parameters.Count - 1; i += 2)
27 | {
28 | var key = parameters[i];
29 | var value = parameters[i + 1];
30 |
31 | if (key.Equals(m_Name, StringComparison.OrdinalIgnoreCase))
32 | {
33 | return value;
34 | }
35 |
36 | if (m_ShortName != null && key.Length > 0 && key.Equals(m_ShortName, StringComparison.OrdinalIgnoreCase))
37 | {
38 | return value;
39 | }
40 | }
41 |
42 | return null;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Dummy/Commands/Tests/CommandTestSpawn.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Threading.Tasks;
4 | using Dummy.API;
5 | using OpenMod.Core.Commands;
6 |
7 | namespace Dummy.Commands.Tests;
8 | [Command("test")]
9 | internal class CommandTestSpawn : Command
10 | {
11 | private readonly IDummyProvider m_DummyProvider;
12 |
13 | public CommandTestSpawn(IServiceProvider serviceProvider, IDummyProvider dummyProvider) : base(serviceProvider)
14 | {
15 | m_DummyProvider = dummyProvider;
16 | }
17 |
18 | protected override async Task OnExecuteAsync()
19 | {
20 | var dummy = await m_DummyProvider.AddDummyAsync(null, null);
21 | var isKicked = await m_DummyProvider.RemoveDummyAsync(dummy.SteamID);
22 | Debug.Assert(isKicked);
23 |
24 | var dummy2 = await m_DummyProvider.AddDummyAsync(null, null);
25 | Debug.Assert(dummy.SteamID.m_SteamID == dummy2.SteamID.m_SteamID);
26 | }
27 | }
--------------------------------------------------------------------------------
/Dummy/ConfigurationEx/ColorTypeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Globalization;
4 | using UnityEngine;
5 |
6 | namespace Dummy.ConfigurationEx;
7 |
8 | internal class ColorTypeConverter : TypeConverter
9 | {
10 | public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
11 | {
12 | return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
13 | }
14 |
15 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
16 | {
17 | return value is string str && ColorUtility.TryParseHtmlString(str, out var color)
18 | ? color
19 | : base.ConvertFrom(context, culture, value);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Dummy/ConfigurationEx/IPAddressTypeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Globalization;
4 | using System.Net;
5 |
6 | namespace Dummy.ConfigurationEx;
7 | internal class IPAddressTypeConverter : TypeConverter
8 | {
9 | public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
10 | {
11 | return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
12 | }
13 |
14 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
15 | {
16 | return value is string address ? IPAddress.Parse(address) : base.ConvertFrom(context, culture, value);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Dummy/ConfigurationEx/NullConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.FileProviders;
5 | using Microsoft.Extensions.Primitives;
6 |
7 | namespace Dummy.ConfigurationEx
8 | {
9 | public sealed class NullConfiguration : IConfiguration
10 | {
11 | public static NullConfiguration Instance { get; } = new();
12 |
13 | private NullConfiguration()
14 | {
15 | }
16 |
17 | public IConfigurationSection GetSection(string key)
18 | {
19 | return default!;
20 | }
21 |
22 | public IEnumerable GetChildren()
23 | {
24 | return Enumerable.Empty();
25 | }
26 |
27 | public IChangeToken GetReloadToken()
28 | {
29 | return NullChangeToken.Singleton;
30 | }
31 |
32 | public string? this[string key]
33 | {
34 | get => default!;
35 | set { }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/Dummy/Dummy.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.ComponentModel;
4 | using System.Net;
5 | using Cysharp.Threading.Tasks;
6 | using Dummy.API;
7 | using Dummy.ConfigurationEx;
8 | using Dummy.Patches;
9 | using HarmonyLib;
10 | using JetBrainsAnnotations::JetBrains.Annotations;
11 | using Microsoft.Extensions.Logging;
12 | using OpenMod.API.Plugins;
13 | using OpenMod.Unturned.Plugins;
14 |
15 | [assembly: PluginMetadata("Dummy", Author = "EvolutionPlugins", DisplayName = "Dummy",
16 | Website = "https://discord.gg/6KymqGv")]
17 |
18 | namespace Dummy
19 | {
20 | internal delegate IDummyProvider NeedDummyProvider();
21 |
22 | [UsedImplicitly]
23 | public class Dummy : OpenModUnturnedPlugin
24 | {
25 | private readonly ILogger m_Logger;
26 | private readonly IDummyProvider m_DummyProvider;
27 |
28 | public Dummy(IServiceProvider serviceProvider, ILogger logger, IDummyProvider dummyProvider) : base(
29 | serviceProvider)
30 | {
31 | m_Logger = logger;
32 | m_DummyProvider = dummyProvider;
33 | }
34 |
35 | protected override UniTask OnLoadAsync()
36 | {
37 | TypeDescriptor.AddAttributes(typeof(UnityEngine.Color), new TypeConverterAttribute(typeof(ColorTypeConverter)));
38 | TypeDescriptor.AddAttributes(typeof(IPAddress), new TypeConverterAttribute(typeof(IPAddressTypeConverter)));
39 |
40 | Patch_Provider.OnNeedDummy += GiveProvider;
41 |
42 | var type = AccessTools.TypeByName("SDG.Unturned.ServerMessageHandler_ReadyToConnect");
43 | var orgMethod = AccessTools.Method(type, "ReadMessage");
44 | var patchMethod =
45 | SymbolExtensions.GetMethodInfo(() => Patch_ServerMessageHandler_ReadyToConnect.ReadMessage(null!));
46 | Harmony.CreateProcessor(orgMethod).AddTranspiler(new HarmonyMethod(patchMethod)).Patch();
47 |
48 | m_Logger.LogInformation("Made with <3 by Evolution Plugins");
49 | m_Logger.LogInformation("Owner of EvolutionPlugins: DiFFoZ");
50 | m_Logger.LogInformation("https://github.com/evolutionplugins \\ https://github.com/diffoz");
51 | m_Logger.LogInformation("Discord Support: https://discord.gg/6KymqGv");
52 |
53 | return UniTask.CompletedTask;
54 | }
55 |
56 | protected override UniTask OnUnloadAsync()
57 | {
58 | Patch_Provider.OnNeedDummy -= GiveProvider;
59 |
60 | return UniTask.CompletedTask;
61 | }
62 |
63 | private IDummyProvider GiveProvider()
64 | {
65 | return m_DummyProvider;
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/Dummy/Dummy.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.1
4 | 10
5 | Dummy
6 | Dummy
7 | enable
8 | nullable
9 |
10 |
11 |
12 | EvolutionPlugins.Dummy
13 | Creates a dummy to help with debugging plugins
14 | openmod openmod-plugin unturned
15 | MIT
16 | DiFFoZ
17 | true
18 | true
19 | https://github.com/EvolutionPlugins/Dummy
20 | git
21 | 0.0.0
22 | 0.0.0
23 | 0.0.0
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Unsafe
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | all
47 | runtime; build; native; contentfiles; analyzers; buildtransitive
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | JetBrainsAnnotations
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Dummy/Events/DummyDeadEvent.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System.Threading.Tasks;
3 | using Cysharp.Threading.Tasks;
4 | using Dummy.API;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using Microsoft.Extensions.Configuration;
7 | using OpenMod.API.Eventing;
8 | using OpenMod.Core.Eventing;
9 | using OpenMod.Unturned.Players;
10 | using OpenMod.Unturned.Players.Life.Events;
11 | using OpenMod.Unturned.Users;
12 | using SDG.NetTransport;
13 | using SDG.Unturned;
14 | using UnityEngine;
15 |
16 | namespace Dummy.Events
17 | {
18 | [UsedImplicitly]
19 | public class DummyDeadEvent : IEventListener
20 | {
21 | private static readonly ClientInstanceMethod s_SendRevive =
22 | ClientInstanceMethod.Get(typeof(PlayerLife), "ReceiveRevive");
23 |
24 | private readonly IDummyProvider m_DummyProvider;
25 | private readonly IUnturnedUserDirectory m_UnturnedUserDirectory;
26 | private readonly IConfiguration m_Configuration;
27 |
28 | public DummyDeadEvent(IDummyProvider dummyProvider, IUnturnedUserDirectory unturnedUserDirectory, IConfiguration configuration)
29 | {
30 | m_DummyProvider = dummyProvider;
31 | m_UnturnedUserDirectory = unturnedUserDirectory;
32 | m_Configuration = configuration;
33 | }
34 |
35 | [EventListener(Priority = EventListenerPriority.Monitor)]
36 | public async Task HandleEventAsync(object? sender, UnturnedPlayerDeathEvent @event)
37 | {
38 | var dummy = await m_DummyProvider.FindDummyUserAsync(@event.Player.SteamId);
39 | if (dummy == null)
40 | {
41 | return;
42 | }
43 |
44 | if (m_Configuration.GetValue("logs:enableDeathLog"))
45 | {
46 | foreach (var owner in dummy.Owners)
47 | {
48 | var player = m_UnturnedUserDirectory.FindUser(owner);
49 | if (player == null)
50 | {
51 | continue;
52 | }
53 |
54 | await player.PrintMessageAsync(
55 | $"Dummy {@event.Player.SteamId} has died. Death reason: {@event.DeathCause.ToString().ToLower()}, killer = {@event.Instigator}. Respawning...");
56 | }
57 | }
58 |
59 | Revive(dummy.Player).Forget();
60 | }
61 |
62 | private static async UniTaskVoid Revive(UnturnedPlayer player)
63 | {
64 | await UniTask.Delay(1500);
65 | if (player.Player == null || player.IsAlive)
66 | {
67 | return;
68 | }
69 |
70 | // todo rewrite to revive and then teleport it back(?)
71 |
72 | var life = player.Player.life;
73 | var transform = life.transform;
74 | life.sendRevive();
75 |
76 | s_SendRevive.InvokeAndLoopback(life.GetNetId(), ENetReliability.Reliable,
77 | Provider.GatherRemoteClientConnections(), transform.position,
78 | MeasurementTool.angleToByte(transform.rotation.eulerAngles.y));
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/Dummy/Events/DummyPlayerDamageEvent.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System.Threading.Tasks;
3 | using Dummy.API;
4 | using Dummy.Users;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Localization;
8 | using OpenMod.API.Eventing;
9 | using OpenMod.Core.Eventing;
10 | using OpenMod.Unturned.Players.Life.Events;
11 | using OpenMod.Unturned.Users;
12 |
13 | namespace Dummy.Events
14 | {
15 | [UsedImplicitly]
16 | public class DummyPlayerDamageEvent : IEventListener,
17 | IEventListener
18 | {
19 | private readonly IDummyProvider m_DummyProvider;
20 | private readonly IConfiguration m_Configuration;
21 | private readonly IStringLocalizer m_StringLocalizer;
22 | private readonly IUnturnedUserDirectory m_UnturnedUserDirectory;
23 |
24 | public DummyPlayerDamageEvent(IDummyProvider dummyProvider, IConfiguration configuration,
25 | IStringLocalizer stringLocalizer, IUnturnedUserDirectory unturnedUserDirectory)
26 | {
27 | m_DummyProvider = dummyProvider;
28 | m_Configuration = configuration;
29 | m_StringLocalizer = stringLocalizer;
30 | m_UnturnedUserDirectory = unturnedUserDirectory;
31 | }
32 |
33 | [EventListener(Priority = EventListenerPriority.Monitor)]
34 | public async Task HandleEventAsync(object? sender, UnturnedPlayerDamagedEvent @event)
35 | {
36 | if (!m_Configuration.GetValue("logs:enableDamageLog"))
37 | {
38 | return;
39 | }
40 |
41 | var dummy = await m_DummyProvider.FindDummyUserAsync(@event.Player.SteamId);
42 | if (dummy == null)
43 | {
44 | return;
45 | }
46 |
47 | var player = m_UnturnedUserDirectory.FindUser(@event.Killer);
48 | if (player == null)
49 | {
50 | return;
51 | }
52 |
53 | await player.PrintMessageAsync(m_StringLocalizer["events:damaged",
54 | new { @event.DamageAmount, dummy.Id }]);
55 | }
56 |
57 | [EventListener(Priority = EventListenerPriority.Normal)]
58 | public async Task HandleEventAsync(object? sender, UnturnedPlayerDamagingEvent @event)
59 | {
60 | var dummy = await m_DummyProvider.FindDummyUserAsync(@event.Player.SteamId);
61 | if (dummy == null)
62 | {
63 | return;
64 | }
65 |
66 | @event.TrackKill = false;
67 | @event.IsCancelled = !m_Configuration.GetValue("events:allowDamage", true);
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/Dummy/Events/DummyServerSendingMessageEvent.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System.Drawing;
3 | using System.Threading.Tasks;
4 | using Dummy.API;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Localization;
8 | using OpenMod.API.Eventing;
9 | using OpenMod.Core.Eventing;
10 | using OpenMod.Unturned.Players.Chat.Events;
11 | using OpenMod.Unturned.Users;
12 |
13 | namespace Dummy.Events
14 | {
15 | [UsedImplicitly]
16 | public class DummyServerSendingMessageEvent : IEventListener
17 | {
18 | private readonly IDummyProvider m_DummyProvider;
19 | private readonly IUnturnedUserDirectory m_UnturnedUserDirectory;
20 | private readonly IStringLocalizer m_StringLocalizer;
21 | private readonly IConfiguration m_Configuration;
22 |
23 | public DummyServerSendingMessageEvent(IDummyProvider dummyProvider, IUnturnedUserDirectory unturnedUserDirectory,
24 | IStringLocalizer stringLocalizer, IConfiguration configuration)
25 | {
26 | m_DummyProvider = dummyProvider;
27 | m_UnturnedUserDirectory = unturnedUserDirectory;
28 | m_StringLocalizer = stringLocalizer;
29 | m_Configuration = configuration;
30 | }
31 |
32 | [EventListener(Priority = EventListenerPriority.Monitor)]
33 | public async Task HandleEventAsync(object? sender, UnturnedServerSendingMessageEvent @event)
34 | {
35 | if (@event.ToPlayer is null)
36 | {
37 | return;
38 | }
39 |
40 | if (!m_Configuration.GetValue("logs:enableChatReceiveLog"))
41 | {
42 | return;
43 | }
44 |
45 | var dummy = await m_DummyProvider.FindDummyUserAsync(@event.ToPlayer.SteamId);
46 | if (dummy == null)
47 | {
48 | return;
49 | }
50 |
51 | foreach (var owner in dummy.Owners)
52 | {
53 | var playerOwner = m_UnturnedUserDirectory.FindUser(owner);
54 | if (playerOwner == null)
55 | {
56 | continue;
57 | }
58 |
59 | await playerOwner.PrintMessageAsync(
60 | m_StringLocalizer["events:chatted", new { @event.Text, dummy.Id }],
61 | Color.Green, @event.IsRich, @event.IconUrl);
62 | }
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/Dummy/Extensions/ConvertorExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Dummy.Extensions
4 | {
5 | public static class ConvertorExtension
6 | {
7 | public static byte[] GetBytes(this string? data)
8 | {
9 | if (data is null)
10 | {
11 | return Array.Empty();
12 | }
13 |
14 | var length = (data.Length + 1) / 3;
15 | var array = new byte[length];
16 | for (var i = 0; i < length; i++)
17 | {
18 | array[i] = Convert.ToByte(data.Substring(3 * i, 2), 16);
19 | }
20 | return array;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Dummy/Extensions/UnturnedExtension.cs:
--------------------------------------------------------------------------------
1 | using Cysharp.Threading.Tasks;
2 | using Dummy.Users;
3 | using OpenMod.Unturned.Users;
4 | using SDG.Unturned;
5 | using UnityEngine;
6 |
7 | namespace Dummy.Extensions
8 | {
9 | public static class UnturnedExtension
10 | {
11 | public static async UniTask TeleportToLocationAsync(this DummyUser dummyUser, Vector3 position, float rotation)
12 | {
13 | await UniTask.SwitchToMainThread();
14 | var b = MeasurementTool.angleToByte(rotation);
15 | dummyUser.Player.Player.ReceiveTeleport(position, b);
16 | return true;
17 | }
18 |
19 | public static async UniTask TeleportToPlayerAsync(this DummyUser from, UnturnedUser to)
20 | {
21 | await UniTask.SwitchToMainThread();
22 |
23 | var transform = to.Player.Player.transform;
24 |
25 | return await from.TeleportToLocationAsync(transform.position, transform.rotation.eulerAngles.y);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Dummy/Models/Clothing.cs:
--------------------------------------------------------------------------------
1 | namespace Dummy.Models;
2 |
3 | ///
4 | /// Skins wrapper.
5 | /// To find skin id open a file EconInfo.json in Unturned folder and find a skin that you like, then copy the value of item_id parameter to set it.
6 | ///
7 | public class Clothing
8 | {
9 | // todo: add support for item_effect
10 |
11 | public uint Shirt { get; set; }
12 | public uint Pants { get; set; }
13 | public uint Hat { get; set; }
14 | public uint Backpack { get; set; }
15 | public uint Vest { get; set; }
16 | public uint Mask { get; set; }
17 | public uint Glasses { get; set; }
18 | }
19 |
--------------------------------------------------------------------------------
/Dummy/Models/Configuration.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using JetBrainsAnnotations::JetBrains.Annotations;
3 |
4 | namespace Dummy.Models
5 | {
6 | [UsedImplicitly]
7 | public class Configuration
8 | {
9 | public ConfigurationOptions Options { get; set; } = new();
10 | public ConfigurationEvents Events { get; set; } = new();
11 | public ConfigurationConnection Connection { get; set; } = new();
12 | public ConfigurationFun Fun { get; set; } = new();
13 | public ConfigurationSettings Default { get; set; } = new();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Dummy/Models/ConfigurationConnection.cs:
--------------------------------------------------------------------------------
1 | namespace Dummy.Models
2 | {
3 | public class ConfigurationConnection
4 | {
5 | public bool RandomIp { get; set; }
6 | public bool RandomPort { get; set; }
7 | }
8 | }
--------------------------------------------------------------------------------
/Dummy/Models/ConfigurationEvents.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using JetBrainsAnnotations::JetBrains.Annotations;
3 |
4 | namespace Dummy.Models
5 | {
6 | [UsedImplicitly]
7 | public class ConfigurationEvents
8 | {
9 | public bool CallOnCheckValidWithExplanation { get; set; }
10 | public bool CallOnCheckBanStatusWithHwid { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/Dummy/Models/ConfigurationFun.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using JetBrainsAnnotations::JetBrains.Annotations;
3 |
4 | namespace Dummy.Models
5 | {
6 | [UsedImplicitly]
7 | public class ConfigurationFun
8 | {
9 | public bool AlwaysRotate { get; set; }
10 | public float RotateYaw { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Dummy/Models/ConfigurationOptions.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using JetBrainsAnnotations::JetBrains.Annotations;
3 |
4 | namespace Dummy.Models
5 | {
6 | [UsedImplicitly]
7 | public class ConfigurationOptions
8 | {
9 | public byte AmountDummies { get; set; }
10 | public bool IsAdmin { get; set; }
11 | public bool CanExecuteCommands { get; set; }
12 | public bool DisableSimulations { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/Dummy/Models/ConfigurationSettings.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 |
3 | using System.Net;
4 | using JetBrainsAnnotations::JetBrains.Annotations;
5 | using SDG.Unturned;
6 | using Steamworks;
7 | using UnityEngine;
8 |
9 | namespace Dummy.Models
10 | {
11 | [UsedImplicitly]
12 | public class ConfigurationSettings
13 | {
14 | public bool IsPro { get; set; } = true;
15 | public byte CharacterId { get; set; }
16 | public string PlayerName { get; set; } = "dummy (steam)";
17 | public string CharacterName { get; set; } = "dummy";
18 | public string NickName { get; set; } = "dummy (group)";
19 | public IPAddress IP { get; set; } = IPAddress.None;
20 | public ushort Port { get; set; }
21 | // todo: add support for multiple hwids
22 | public string? Hwid { get; set; }
23 | public CSteamID SteamGroupId { get; set; }
24 | public Color SkinColor { get; set; }
25 | public byte BeardId { get; set; }
26 | public Color BeardColor { get; set; }
27 | public byte HairId { get; set; }
28 | public Color HairColor { get; set; }
29 | public byte FaceId { get; set; }
30 | public Color MarkerColor { get; set; }
31 | public bool IsLeftHanded { get; set; }
32 | public Clothing Skins { get; set; } = new();
33 | public EPlayerSkillset PlayerSkillset { get; set; }
34 | public string Language { get; set; } = "english";
35 | public CSteamID LobbyId { get; set; }
36 | }
37 | }
--------------------------------------------------------------------------------
/Dummy/NetTransports/DummyTransportConnection.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Dummy.Models;
3 | using Microsoft.Extensions.Configuration;
4 | using OpenMod.API.Plugins;
5 | using SDG.NetTransport;
6 | using SDG.Unturned;
7 |
8 | namespace Dummy.NetTransports
9 | {
10 | public class DummyTransportConnection : ITransportConnection
11 | {
12 | private readonly string m_IP;
13 | private readonly ushort m_Port;
14 | private readonly IPAddress m_Address;
15 |
16 | public DummyTransportConnection(IPluginAccessor pluginAccessor)
17 | {
18 | var random = new System.Random();
19 | var config = pluginAccessor.Instance!.Configuration.Get();
20 |
21 | m_IP = config!.Connection.RandomIp || config.Default.IP == null
22 | ? $"{random.Next(1, 256)}.{random.Next(256)}.{random.Next(256)}.{random.Next(256)}"
23 | : (m_Address = config.Default.IP).ToString();
24 | m_Address ??= IPAddress.Parse(m_IP);
25 |
26 | m_Port = config.Connection.RandomPort
27 | ? (ushort)random.Next(IPEndPoint.MinPort + 1, IPEndPoint.MaxPort + 1)
28 | : config.Default.Port;
29 | }
30 |
31 | public void CloseConnection()
32 | {
33 | }
34 |
35 | public bool Equals(ITransportConnection other)
36 | {
37 | return Equals(this, other);
38 | }
39 |
40 | public bool TryGetIPv4Address(out uint address)
41 | {
42 | address = Parser.getUInt32FromIP(m_IP);
43 | return true;
44 | }
45 |
46 | public bool TryGetPort(out ushort port)
47 | {
48 | port = m_Port;
49 | return true;
50 | }
51 |
52 | public IPAddress GetAddress()
53 | {
54 | return m_Address;
55 | }
56 |
57 | public string GetAddressString(bool withPort)
58 | {
59 | return m_IP + (withPort ? ":" + m_Port : string.Empty);
60 | }
61 |
62 | public void Send(byte[] buffer, long size, ENetReliability sendType)
63 | {
64 | var invokableReader = NetMessages.GetInvokableReader();
65 | invokableReader.SetBufferSegment(buffer, (int)size);
66 | invokableReader.Reset();
67 |
68 | invokableReader.ReadEnum(out EClientMessage clientMessage);
69 | if (clientMessage is not EClientMessage.InvokeMethod)
70 | {
71 | return;
72 | }
73 |
74 | invokableReader.ReadBits(NetReflection.clientMethodsBitCount, out var num);
75 | if (num >= NetReflection.clientMethodsLength)
76 | return;
77 |
78 | var method = NetReflection.clientMethods[(int)num];
79 |
80 | if (method.declaringType == typeof(PlayerInput)
81 | && method.debugName.Contains("ReceiveSimulateMispredictedInputs"))
82 | {
83 | var cic = new ClientInvocationContext(ClientInvocationContext.EOrigin.Remote, invokableReader, method);
84 | method.readMethod(in cic);
85 | }
86 | }
87 |
88 | public bool TryGetSteamId(out ulong steamId)
89 | {
90 | steamId = 0;
91 | return false;
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/Dummy/Patches/Patch_InteractableVehicle.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using HarmonyLib;
3 | using SDG.Unturned;
4 | using UnityEngine;
5 |
6 | namespace Dummy.Patches;
7 | [HarmonyPatch(typeof(InteractableVehicle))]
8 | internal static class Patch_InteractableVehicle
9 | {
10 | [HarmonyReversePatch]
11 | [HarmonyPatch("PackRoadPosition")]
12 | public static Vector3 PackRoadPositionOriginal(float roadPosition)
13 | {
14 | throw new NotImplementedException("It's a stub");
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Dummy/Patches/Patch_Provider.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using System.Reflection.Emit;
6 | using HarmonyLib;
7 | using JetBrainsAnnotations::JetBrains.Annotations;
8 | using SDG.Unturned;
9 |
10 | namespace Dummy.Patches
11 | {
12 | // ReSharper disable InconsistentNaming
13 | [HarmonyPatch(typeof(Provider))]
14 | internal static class Patch_Provider
15 | {
16 | internal static event NeedDummyProvider? OnNeedDummy;
17 |
18 | public static int GetDummiesCount()
19 | {
20 | return OnNeedDummy?.Invoke().Dummies.Count ?? 0;
21 | }
22 |
23 | internal static readonly MethodInfo s_GetClients = AccessTools.DeclaredPropertyGetter(typeof(Provider), "clients");
24 | internal static readonly MethodInfo s_GetDummiesCount = SymbolExtensions.GetMethodInfo(() => GetDummiesCount());
25 |
26 | [HarmonyPrefix]
27 | [HarmonyPatch("battlEyeServerKickPlayer")]
28 | [UsedImplicitly]
29 | public static bool battlEyeServerKickPlayer(int playerID)
30 | {
31 | return !(OnNeedDummy?.Invoke().Dummies.Any(x => x.Player.BattlEyeId == playerID) ?? false);
32 | }
33 |
34 | [HarmonyTranspiler]
35 | [HarmonyPatch("verifyNextPlayerInQueue")]
36 | [UsedImplicitly]
37 | public static IEnumerable verifyNextPlayerInQueue(IEnumerable instructions)
38 | {
39 | var codes = new List(instructions);
40 | for (var i = 0; i < codes.Count; i++)
41 | {
42 | var instruction = codes[i];
43 | if (instruction.opcode != OpCodes.Call || !instruction.Calls(s_GetClients))
44 | {
45 | continue;
46 | }
47 |
48 | i += 2;
49 |
50 | codes.Insert(i, new(OpCodes.Call, s_GetDummiesCount));
51 | codes.Insert(i + 1, new(OpCodes.Sub));
52 | break;
53 | }
54 | return codes;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Dummy/Patches/Patch_ServerMessageHandler_ReadyToConnect.cs:
--------------------------------------------------------------------------------
1 | using HarmonyLib;
2 | using SDG.Unturned;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Reflection.Emit;
6 |
7 | namespace Dummy.Patches
8 | {
9 | // ReSharper disable once InconsistentNaming
10 | internal static class Patch_ServerMessageHandler_ReadyToConnect
11 | {
12 | public static IEnumerable ReadMessage(IEnumerable instructions)
13 | {
14 | var providerMaxPlayers = typeof(Provider).GetProperty(nameof(Provider.maxPlayers), AccessTools.all)?.GetGetMethod()
15 | ?? throw new NullReferenceException("providerMaxPlayers is null");
16 |
17 | var codes = new List(instructions);
18 | var index = codes.FindIndex(x => x.Calls(providerMaxPlayers));
19 | if (index < 0)
20 | {
21 | return codes;
22 | }
23 |
24 | // insert after that IL
25 | index++;
26 |
27 | codes.InsertRange(index, new[]
28 | {
29 | new CodeInstruction(OpCodes.Call, Patch_Provider.s_GetDummiesCount),
30 | new CodeInstruction(OpCodes.Sub),
31 | });
32 |
33 | return codes;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Dummy/Permissions/DummyPermissionCheckProvider.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Dummy.Models;
3 | using Dummy.Users;
4 | using JetBrainsAnnotations::JetBrains.Annotations;
5 | using Microsoft.Extensions.Configuration;
6 | using OpenMod.API.Plugins;
7 | using OpenMod.API.Prioritization;
8 | using OpenMod.Core.Permissions;
9 |
10 | namespace Dummy.Permissions
11 | {
12 | [Priority(Priority = Priority.High)]
13 | [UsedImplicitly]
14 | public class DummyPermissionCheckProvider : AlwaysGrantPermissionCheckProvider
15 | {
16 | public DummyPermissionCheckProvider(IPluginAccessor pluginAccessor) : base((actor) =>
17 | {
18 | if (actor is not DummyUser user)
19 | {
20 | return false;
21 | }
22 |
23 | if (user.SteamPlayer.isAdmin)
24 | {
25 | return true;
26 | }
27 |
28 | var options = pluginAccessor.Instance!.Configuration.GetValue("options");
29 | return options!.CanExecuteCommands || options.IsAdmin;
30 | })
31 | {
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/Dummy/Players/DummyPlayer.cs:
--------------------------------------------------------------------------------
1 | using Dummy.Users;
2 | using HarmonyLib;
3 | using OpenMod.Unturned.Players;
4 | using SDG.Unturned;
5 | using System;
6 | using System.Reflection;
7 |
8 | namespace Dummy.Players
9 | {
10 | public class DummyPlayer : UnturnedPlayer, IEquatable
11 | {
12 | private static readonly FieldInfo s_BattlEyeId = AccessTools.Field(typeof(SteamPlayer), "battlEyeId");
13 |
14 | public int BattlEyeId { get; }
15 |
16 | public DummyPlayer(SteamPlayer steamPlayer) : base(steamPlayer.player)
17 | {
18 | BattlEyeId = (int)s_BattlEyeId.GetValue(steamPlayer);
19 | }
20 |
21 | public bool Equals(DummyUser other)
22 | {
23 | if (ReferenceEquals(null, other)) return false;
24 | if (ReferenceEquals(this, other)) return true;
25 |
26 | return other.SteamID.Equals(SteamId);
27 | }
28 |
29 | public override bool Equals(object? obj)
30 | {
31 | if (obj is DummyPlayer other) return Equals(other);
32 | else return false;
33 | }
34 |
35 | public override int GetHashCode()
36 | {
37 | return -975366258 + SteamId.GetHashCode();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Dummy/ServiceConfigurator.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using Dummy.NetTransports;
3 | using Dummy.Permissions;
4 | using Dummy.Services;
5 | using JetBrainsAnnotations::JetBrains.Annotations;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using OpenMod.API.Ioc;
8 | using OpenMod.Core.Permissions;
9 | using OpenMod.Core.Users;
10 | using SDG.NetTransport;
11 |
12 | namespace Dummy
13 | {
14 | [UsedImplicitly]
15 | public class ServiceConfigurator : IServiceConfigurator
16 | {
17 | public void ConfigureServices(IOpenModServiceConfigurationContext openModStartupContext,
18 | IServiceCollection serviceCollection)
19 | {
20 | serviceCollection.Configure(options =>
21 | options.AddPermissionCheckProvider());
22 | serviceCollection.AddTransient();
23 | serviceCollection.Configure(options => options.AddUserProvider());
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/Dummy/Services/DummyProvider.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Cysharp.Threading.Tasks;
6 | using Dummy.API;
7 | using Dummy.Models;
8 | using Dummy.Users;
9 | using JetBrainsAnnotations::JetBrains.Annotations;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using OpenMod.API.Ioc;
12 | using OpenMod.API.Prioritization;
13 | using OpenMod.API.Users;
14 | using OpenMod.Unturned.Users;
15 | using Steamworks;
16 |
17 | namespace Dummy.Services
18 | {
19 | [ServiceImplementation(Lifetime = ServiceLifetime.Singleton, Priority = Priority.Lowest)]
20 | [UsedImplicitly]
21 | public class DummyProvider : IDummyProvider
22 | {
23 | private readonly DummyUserProvider m_Provider;
24 |
25 | public IReadOnlyCollection Dummies => m_Provider.DummyUsers;
26 |
27 | public DummyProvider(IUserManager userManager)
28 | {
29 | m_Provider = (DummyUserProvider)userManager.UserProviders.First(x => x is DummyUserProvider)!;
30 | }
31 |
32 | public Task AddDummyAsync(CSteamID? id, HashSet? owners)
33 | {
34 | return m_Provider.AddDummyAsync(id, owners);
35 | }
36 |
37 | public Task AddCopiedDummyAsync(CSteamID? id, HashSet? owners, UnturnedUser? userCopy)
38 | {
39 | return m_Provider.AddDummyAsync(id, owners, userCopy);
40 | }
41 |
42 | public Task AddDummyByParameters(CSteamID? id, HashSet? owners,
43 | ConfigurationSettings? settings)
44 | {
45 | return m_Provider.AddDummyAsync(id, owners, settings: settings);
46 | }
47 |
48 | public Task AddDummyAsync(CSteamID? id, HashSet? owners = null, UnturnedUser? userCopy = null, ConfigurationSettings? settings = null)
49 | {
50 | return m_Provider.AddDummyAsync(id, owners, userCopy: userCopy, settings: settings);
51 | }
52 |
53 | public async Task RemoveDummyAsync(CSteamID id)
54 | {
55 | await UniTask.SwitchToMainThread();
56 |
57 | var playerDummy = await FindDummyUserAsync(id);
58 | if (playerDummy == null)
59 | {
60 | return false;
61 | }
62 |
63 | m_Provider.DummyUsers.Remove(playerDummy);
64 | await playerDummy.DisposeAsync();
65 |
66 | return true;
67 | }
68 |
69 | public async Task ClearDummiesAsync()
70 | {
71 | await UniTask.SwitchToMainThread();
72 |
73 | for (var i = m_Provider.DummyUsers.Count - 1; i >= 0; i--)
74 | {
75 | var user = m_Provider.DummyUsers.ElementAt(i);
76 |
77 | m_Provider.DummyUsers.Remove(user);
78 | await user.DisposeAsync();
79 | }
80 | }
81 |
82 | public Task FindDummyUserAsync(CSteamID id)
83 | {
84 | return Task.FromResult(Dummies.FirstOrDefault(x => x.SteamID == id));
85 | }
86 |
87 | public Task FindDummyUserAsync(ulong id)
88 | {
89 | return FindDummyUserAsync((CSteamID)id);
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/Dummy/Services/DummyUserProvider.cs:
--------------------------------------------------------------------------------
1 | extern alias JetBrainsAnnotations;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Security.Cryptography;
6 | using System.Threading.Tasks;
7 | using Autofac;
8 | using Cysharp.Threading.Tasks;
9 | using Dummy.API.Exceptions;
10 | using Dummy.ConfigurationEx;
11 | using Dummy.Extensions;
12 | using Dummy.Models;
13 | using Dummy.Users;
14 | using JetBrainsAnnotations::JetBrains.Annotations;
15 | using Microsoft.Extensions.Configuration;
16 | using Microsoft.Extensions.Localization;
17 | using Microsoft.Extensions.Logging;
18 | using MoreLinq;
19 | using OpenMod.API.Plugins;
20 | using OpenMod.API.Users;
21 | using OpenMod.Core.Helpers;
22 | using OpenMod.Core.Localization;
23 | using OpenMod.Core.Users;
24 | using OpenMod.Unturned.Users;
25 | using SDG.NetTransport;
26 | using SDG.Unturned;
27 | using Steamworks;
28 | using UnityEngine;
29 | using Color = System.Drawing.Color;
30 | using UColor = UnityEngine.Color;
31 |
32 | namespace Dummy.Services
33 | {
34 | [UsedImplicitly]
35 | public class DummyUserProvider : IUserProvider, IAsyncDisposable
36 | {
37 | private readonly IPluginAccessor m_PluginAccessor;
38 | private readonly IUserDataStore m_UserDataStore;
39 | private readonly ILogger m_Logger;
40 | private readonly ILoggerFactory m_LoggerFactory;
41 | private readonly ILifetimeScope m_LifetimeScope;
42 |
43 | private UnturnedUserProvider UserProvider => m_LifetimeScope.Resolve().UserProviders
44 | .OfType().FirstOrDefault()!;
45 |
46 | public HashSet DummyUsers { get; }
47 |
48 | private bool m_Disposed;
49 | private bool m_IsShuttingDown;
50 |
51 | public DummyUserProvider(IPluginAccessor pluginAccessor, IUserDataStore userDataStore,
52 | ILogger logger, ILoggerFactory loggerFactory, ILifetimeScope lifetimeScope)
53 | {
54 | m_PluginAccessor = pluginAccessor;
55 | m_UserDataStore = userDataStore;
56 | m_Logger = logger;
57 | m_LoggerFactory = loggerFactory;
58 | m_LifetimeScope = lifetimeScope;
59 | DummyUsers = new();
60 |
61 | Provider.onCommenceShutdown += ProviderOnonCommenceShutdown;
62 | Provider.onServerDisconnected += OnServerDisconnected;
63 |
64 | AsyncHelper.Schedule("Do not auto kick a dummies", DontAutoKickTask);
65 | }
66 |
67 | private void OnServerDisconnected(CSteamID steamID)
68 | {
69 | var dummy = DummyUsers.FirstOrDefault(x => x.SteamId == steamID);
70 | if (dummy == null)
71 | {
72 | return;
73 | }
74 |
75 | if (DummyUsers.Remove(dummy))
76 | {
77 | dummy.Actions.Enabled = false;
78 | dummy.Simulation.Enabled = false;
79 |
80 | // Because DummyUser.DisposeAsync() calls Session.DisconnectAsync with will call Provider.kick it will throw exception that clients list was modified in kick method.
81 | // So we will dispose it on frame later. But simulation and actions will be already disabled.
82 | DelayDispose(dummy).Forget();
83 | }
84 | }
85 |
86 | private static async UniTaskVoid DelayDispose(DummyUser dummy)
87 | {
88 | await UniTask.NextFrame();
89 | await dummy.DisposeAsync();
90 | }
91 |
92 | private void ProviderOnonCommenceShutdown()
93 | {
94 | m_IsShuttingDown = true;
95 | }
96 |
97 | public bool SupportsUserType(string userType)
98 | {
99 | return userType.Equals(KnownActorTypes.Player, StringComparison.OrdinalIgnoreCase);
100 | }
101 |
102 | public Task FindUserAsync(string userType, string searchString, UserSearchMode searchMode)
103 | {
104 | if (!SupportsUserType(userType))
105 | {
106 | return Task.FromResult(null);
107 | }
108 |
109 | DummyUser? dummyUser = null;
110 | var confidence = 0;
111 |
112 | foreach (var user in DummyUsers)
113 | {
114 | switch (searchMode)
115 | {
116 | case UserSearchMode.FindByNameOrId:
117 | case UserSearchMode.FindById:
118 | if (user.Id.Equals(searchString, StringComparison.OrdinalIgnoreCase))
119 | {
120 | return Task.FromResult(user);
121 | }
122 |
123 | if (searchMode == UserSearchMode.FindByNameOrId)
124 | {
125 | goto case UserSearchMode.FindByName;
126 | }
127 | break;
128 |
129 | case UserSearchMode.FindByName:
130 | var currentConfidence = NameConfidence(user.DisplayName, searchString, confidence);
131 | if (currentConfidence > confidence)
132 | {
133 | dummyUser = user;
134 | confidence = currentConfidence;
135 | }
136 | break;
137 | default:
138 | return Task.FromException(new ArgumentOutOfRangeException(nameof(searchMode), searchMode,
139 | null));
140 | }
141 | }
142 |
143 | return Task.FromResult(dummyUser);
144 | }
145 |
146 | private static int NameConfidence(string userName, string searchName, int currentConfidence = -1)
147 | {
148 | switch (currentConfidence)
149 | {
150 | case 2:
151 | if (userName.Equals(searchName, StringComparison.OrdinalIgnoreCase))
152 | return 3;
153 | goto case 1;
154 |
155 | case 1:
156 | if (userName.StartsWith(searchName, StringComparison.OrdinalIgnoreCase))
157 | return 2;
158 | goto case 0;
159 |
160 | case 0:
161 | if (userName.IndexOf(searchName, StringComparison.OrdinalIgnoreCase) != -1)
162 | return 1;
163 | break;
164 |
165 | default:
166 | goto case 2;
167 | }
168 |
169 | return -1;
170 | }
171 |
172 | public Task> GetUsersAsync(string userType)
173 | {
174 | return !SupportsUserType(userType)
175 | ? Task.FromResult((IReadOnlyCollection)Enumerable.Empty())
176 | : Task.FromResult>(DummyUsers);
177 | }
178 |
179 | public async Task BroadcastAsync(string message, Color? color = null)
180 | {
181 | var sColor = color ?? Color.White;
182 |
183 | foreach (var user in DummyUsers)
184 | {
185 | await user.PrintMessageAsync(message, sColor);
186 | }
187 | }
188 |
189 | public Task BroadcastAsync(string userType, string message, Color? color = null)
190 | {
191 | return !SupportsUserType(userType) ? Task.CompletedTask : BroadcastAsync(message, color);
192 | }
193 |
194 | public Task BanAsync(IUser user, string? reason = null, DateTime? endTime = null)
195 | {
196 | return BanAsync(user, null, reason, endTime);
197 | }
198 |
199 | public async Task BanAsync(IUser user, IUser? instigator = null, string? reason = null,
200 | DateTime? endTime = null)
201 | {
202 | if (user is not DummyUser dummy)
203 | {
204 | return false;
205 | }
206 |
207 | reason ??= "No reason provided";
208 | endTime ??= DateTime.MaxValue;
209 | if (!ulong.TryParse(instigator?.Id, out var instigatorId))
210 | {
211 | instigatorId = 0;
212 | }
213 |
214 | var banDuration = (uint)(endTime.Value - DateTime.Now).TotalSeconds;
215 | if (banDuration <= 0)
216 | return false;
217 |
218 | await UniTask.SwitchToMainThread();
219 | var ip = dummy.SteamPlayer.getIPv4AddressOrZero();
220 | return Provider.requestBanPlayer((CSteamID)instigatorId, dummy.SteamID, ip, dummy.SteamPlayer.playerID.GetHwids(), reason, banDuration);
221 | }
222 |
223 | public async Task KickAsync(IUser user, string? reason = null)
224 | {
225 | if (user is not DummyUser dummy)
226 | {
227 | return false;
228 | }
229 |
230 | await dummy.Session?.DisconnectAsync(reason!)!;
231 | return true;
232 | }
233 |
234 | public async Task AddDummyAsync(CSteamID? id, HashSet? owners = null,
235 | UnturnedUser? userCopy = null, ConfigurationSettings? settings = null)
236 | {
237 | var localizer = m_PluginAccessor.Instance?.LifetimeScope.Resolve() ??
238 | NullStringLocalizer.Instance;
239 | var configuration = m_PluginAccessor.Instance?.Configuration ?? NullConfiguration.Instance;
240 | var config = configuration.Get();
241 |
242 | owners ??= new();
243 |
244 | var sId = id ?? GetAvailableId();
245 |
246 | ValidateSpawn(sId);
247 |
248 | SteamPlayerID playerID;
249 | SteamPending pending;
250 |
251 | if (userCopy != null)
252 | {
253 | // settings should be in priority
254 |
255 | var userSteamPlayer = userCopy.Player.SteamPlayer;
256 | var uPlayerID = userCopy.Player.SteamPlayer.playerID;
257 | playerID = new(sId, settings?.CharacterId ?? uPlayerID.characterID,
258 | settings?.PlayerName ?? uPlayerID.playerName,
259 | settings?.CharacterName ?? uPlayerID.characterName,
260 | settings?.NickName ?? uPlayerID.nickName,
261 | settings?.SteamGroupId ?? uPlayerID.group);
262 |
263 | pending = new(GetTransportConnection(), playerID, userSteamPlayer.isPro,
264 | userSteamPlayer.face, userSteamPlayer.hair, userSteamPlayer.beard, userSteamPlayer.skin,
265 | userSteamPlayer.color, userSteamPlayer.markerColor, userSteamPlayer.IsLeftHanded,
266 | (ulong)userSteamPlayer.shirtItem, (ulong)userSteamPlayer.pantsItem, (ulong)userSteamPlayer.hatItem,
267 | (ulong)userSteamPlayer.backpackItem, (ulong)userSteamPlayer.vestItem,
268 | (ulong)userSteamPlayer.maskItem, (ulong)userSteamPlayer.glassesItem, Array.Empty(),
269 | userSteamPlayer.skillset, userSteamPlayer.language, userSteamPlayer.lobbyID, EClientPlatform.Windows)
270 | {
271 | hasProof = true,
272 | hasGroup = true,
273 | hasAuthentication = true
274 | };
275 | }
276 | else
277 | {
278 | settings ??= config!.Default;
279 | if (settings is null)
280 | {
281 | throw new ArgumentNullException(nameof(settings));
282 | }
283 |
284 | var skins = settings.Skins ?? throw new ArgumentException(nameof(settings.Skins));
285 |
286 | var skinColor = settings.SkinColor;
287 | var hairColor = settings.HairColor;
288 | var markerColor = settings.MarkerColor;
289 | var hwid = settings.Hwid.GetBytes();
290 |
291 | if (hwid.Length != 20)
292 | {
293 | hwid = new byte[20];
294 |
295 | using var rng = RandomNumberGenerator.Create();
296 | rng.GetBytes(hwid);
297 | }
298 |
299 | playerID = new(sId, settings.CharacterId, settings.PlayerName,
300 | settings.CharacterName, settings.NickName, settings.SteamGroupId, settings.Hwid.GetBytes());
301 |
302 | pending = new(GetTransportConnection(), playerID, settings.IsPro, settings.FaceId,
303 | settings.HairId, settings.BeardId, skinColor, hairColor, markerColor,
304 | settings.IsLeftHanded, skins.Shirt, skins.Pants, skins.Hat,
305 | skins.Backpack, skins.Vest, skins.Mask, skins.Glasses, Array.Empty(),
306 | settings.PlayerSkillset, settings.Language, settings.LobbyId, EClientPlatform.Windows)
307 | {
308 | hasAuthentication = true,
309 | hasGroup = true,
310 | hasProof = true
311 | };
312 | }
313 |
314 | PrepareInventoryDetails(pending);
315 |
316 | var logJoinLeave = configuration.GetSection("logs:enableJoinLeaveLog").Get();
317 |
318 | await PreAddDummyAsync(pending, config!.Events!, logJoinLeave);
319 | try
320 | {
321 | await UniTask.SwitchToMainThread();
322 |
323 | Provider.accept(playerID, pending!.assignedPro, pending.assignedAdmin, pending.face,
324 | pending.hair, pending.beard, pending.skin, pending.color, pending.markerColor, pending.hand,
325 | pending.shirtItem, pending.pantsItem, pending.hatItem, pending.backpackItem, pending.vestItem,
326 | pending.maskItem, pending.glassesItem, pending.skinItems, pending.skinTags,
327 | pending.skinDynamicProps, pending.skillset, pending.language, pending.lobbyID, EClientPlatform.Windows);
328 |
329 | var probDummyPlayer = Provider.clients.LastOrDefault();
330 | if (probDummyPlayer?.playerID.steamID != playerID.steamID)
331 | {
332 | throw new DummyCanceledSpawnException(
333 | $"Plugin or Game rejected connection of a dummy {pending.playerID.steamID}");
334 | }
335 |
336 | var options = config.Options ?? throw new ArgumentNullException(nameof(config.Options));
337 | var fun = config.Fun ?? throw new ArgumentNullException(nameof(config.Fun));
338 |
339 | var dummyUser = new DummyUser(UserProvider, m_UserDataStore, probDummyPlayer, m_LoggerFactory, localizer,
340 | options.DisableSimulations, owners);
341 |
342 | PostAddDummy(dummyUser, fun, logJoinLeave);
343 | return dummyUser;
344 | }
345 | catch (DummyCanceledSpawnException)
346 | {
347 | Provider.pending.Remove(pending);
348 | throw;
349 | }
350 | catch
351 | {
352 | var i = Provider.clients.FindIndex(x => x.playerID == playerID);
353 | if (i >= 0)
354 | {
355 | Provider.clients.RemoveAt(i);
356 | }
357 |
358 | throw;
359 | }
360 | }
361 |
362 | private async UniTask PreAddDummyAsync(SteamPending pending, ConfigurationEvents events, bool logJoinLeave)
363 | {
364 | await UniTask.SwitchToMainThread();
365 |
366 | Provider.pending.Add(pending);
367 |
368 | if (CommandWindow.shouldLogJoinLeave && !logJoinLeave)
369 | CommandWindow.shouldLogJoinLeave = logJoinLeave;
370 |
371 | if (events.CallOnCheckBanStatusWithHwid)
372 | {
373 | pending.transportConnection.TryGetIPv4Address(out var ip);
374 | var isBanned = false;
375 | var banReason = string.Empty;
376 | var banRemainingDuration = 0U;
377 | if (SteamBlacklist.checkBanned(pending.playerID.steamID, ip, pending.playerID.GetHwids(), out var steamBlacklistID))
378 | {
379 | isBanned = true;
380 | banReason = steamBlacklistID!.reason;
381 | banRemainingDuration = steamBlacklistID.getTime();
382 | }
383 |
384 | try
385 | {
386 | Provider.onCheckBanStatusWithHWID?.Invoke(pending.playerID, ip, ref isBanned, ref banReason,
387 | ref banRemainingDuration);
388 | }
389 | catch (Exception e)
390 | {
391 | m_Logger.LogError(e, "Plugin raised an exception from onCheckBanStatusWithHWID");
392 | }
393 |
394 | if (isBanned)
395 | {
396 | Provider.pending.Remove(pending);
397 | throw new DummyCanceledSpawnException(
398 | $"Dummy {pending.playerID.steamID} is banned! Ban reason: {banReason}, duration: {banRemainingDuration}");
399 | }
400 | }
401 |
402 | if (events.CallOnCheckValidWithExplanation)
403 | {
404 | var isValid = true;
405 | var explanation = string.Empty;
406 | try
407 | {
408 | Provider.onCheckValidWithExplanation(new()
409 | {
410 | m_SteamID = pending.playerID.steamID,
411 | m_eAuthSessionResponse = EAuthSessionResponse.k_EAuthSessionResponseOK,
412 | m_OwnerSteamID = pending.playerID.steamID
413 | }, ref isValid, ref explanation);
414 | }
415 | catch (Exception e)
416 | {
417 | m_Logger.LogError(e, "Plugin raised an exception from onCheckValidWithExplanation");
418 | }
419 |
420 | if (!isValid)
421 | {
422 | Provider.pending.Remove(pending);
423 | throw new DummyCanceledSpawnException(
424 | $"Plugin reject connection of a dummy {pending.playerID.steamID}. Reason: {explanation}");
425 | }
426 | }
427 | }
428 |
429 | private static void PrepareInventoryDetails(SteamPending pending)
430 | {
431 | pending.shirtItem = (int)pending.packageShirt;
432 | pending.pantsItem = (int)pending.packagePants;
433 | pending.hatItem = (int)pending.packageHat;
434 | pending.backpackItem = (int)pending.packageBackpack;
435 | pending.vestItem = (int)pending.packageVest;
436 | pending.maskItem = (int)pending.packageMask;
437 | pending.glassesItem = (int)pending.packageGlasses;
438 | pending.skinItems = Array.Empty();
439 | pending.skinTags = Array.Empty();
440 | pending.skinDynamicProps = Array.Empty();
441 | }
442 |
443 | private void PostAddDummy(DummyUser playerDummy, ConfigurationFun fun, bool logJoinLeave)
444 | {
445 | if (fun.AlwaysRotate)
446 | {
447 | RotateDummyTask(playerDummy, fun.RotateYaw).Forget();
448 | }
449 |
450 | if (!CommandWindow.shouldLogJoinLeave && !logJoinLeave)
451 | CommandWindow.shouldLogJoinLeave = true;
452 |
453 | DummyUsers.Add(playerDummy);
454 | }
455 |
456 | private async UniTaskVoid RotateDummyTask(DummyUser player, float rotateYaw)
457 | {
458 | while (!m_Disposed && player.Simulation.Enabled)
459 | {
460 | await UniTask.Delay(1);
461 | player.Simulation.SetRotation(player.Player.Player.look.yaw + rotateYaw, player.Player.Player.look.pitch, 1f);
462 | }
463 | }
464 |
465 | [AssertionMethod]
466 | private void ValidateSpawn(CSteamID id)
467 | {
468 | var localizer = m_PluginAccessor.Instance?.LifetimeScope.Resolve() ??
469 | NullStringLocalizer.Instance;
470 | var configuration = m_PluginAccessor.Instance?.Configuration ?? NullConfiguration.Instance;
471 |
472 | if (Provider.clients.Any(x => x.playerID.steamID == id))
473 | {
474 | throw new DummyContainsException(localizer, id.m_SteamID); // or id is taken
475 | }
476 |
477 | var amountDummiesConfig = configuration.Get()!.Options?.AmountDummies ?? 0;
478 | if (amountDummiesConfig != 0 && DummyUsers.Count + 1 > amountDummiesConfig)
479 | {
480 | throw new DummyOverflowsException(localizer, (byte)DummyUsers.Count, amountDummiesConfig);
481 | }
482 | }
483 |
484 | private async Task DontAutoKickTask()
485 | {
486 | var time = (int)Math.Floor(Provider.configData.Server.Timeout_Game_Seconds * 0.5f);
487 |
488 | while (!m_Disposed)
489 | {
490 | foreach (var dummy in DummyUsers.ToArray())
491 | {
492 | dummy.SteamPlayer.timeLastPacketWasReceivedFromClient = Time.realtimeSinceStartup;
493 | }
494 |
495 | await Task.Delay(time);
496 | }
497 | }
498 |
499 | public virtual CSteamID GetAvailableId()
500 | {
501 | var result = new CSteamID(1);
502 |
503 | while (DummyUsers.Any(x => x.SteamID == result))
504 | {
505 | result.m_SteamID++;
506 | }
507 |
508 | return result;
509 | }
510 |
511 | public virtual ITransportConnection GetTransportConnection()
512 | {
513 | return m_LifetimeScope.Resolve();
514 | }
515 |
516 | public async ValueTask DisposeAsync()
517 | {
518 | if (m_Disposed)
519 | {
520 | return;
521 | }
522 |
523 | m_Disposed = true;
524 | Provider.onCommenceShutdown -= ProviderOnonCommenceShutdown;
525 | Provider.onServerDisconnected -= OnServerDisconnected;
526 |
527 | if (!m_IsShuttingDown)
528 | {
529 | foreach (var user in DummyUsers)
530 | {
531 | await user.DisposeAsync();
532 | }
533 | }
534 | }
535 | }
536 | }
--------------------------------------------------------------------------------
/Dummy/Threads/DummyUserActionThread.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Cysharp.Threading.Tasks;
7 | using Dummy.API;
8 | using Dummy.Users;
9 | using Microsoft.Extensions.Logging;
10 | using SDG.Unturned;
11 |
12 | namespace Dummy.Threads
13 | {
14 | public class DummyUserActionThread : IAsyncDisposable
15 | {
16 | public ConcurrentQueue Actions { get; }
17 | public List ContinuousActions { get; }
18 |
19 | private readonly DummyUser m_Dummy;
20 | private readonly ILogger m_Logger;
21 |
22 | public DummyUserActionThread(DummyUser dummy, ILogger logger)
23 | {
24 | Actions = new ConcurrentQueue();
25 | ContinuousActions = new List();
26 | m_Dummy = dummy;
27 | m_Logger = logger;
28 | }
29 |
30 | public bool Enabled { get; set; }
31 |
32 | internal async UniTask ExecuteActions()
33 | {
34 | if (!Enabled)
35 | {
36 | return;
37 | }
38 |
39 | try
40 | {
41 | foreach (var action in ContinuousActions)
42 | {
43 | if (action == null)
44 | continue;
45 |
46 | await action.Do(m_Dummy);
47 | }
48 |
49 | if (!Actions.IsEmpty)
50 | {
51 | if (!Actions.TryDequeue(out var action) || action == null)
52 | return;
53 |
54 | await action.Do(m_Dummy);
55 | }
56 | }
57 | catch (Exception e)
58 | {
59 | m_Logger.LogError(e, "Exception on Dummy action thread");
60 | }
61 | }
62 |
63 | public ValueTask DisposeAsync()
64 | {
65 | Enabled = false;
66 | return new();
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/Dummy/Threads/DummyUserSimulationThread.Equipment.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Dummy.Actions.Interaction;
3 | using SDG.Unturned;
4 | using UnityEngine;
5 |
6 | namespace Dummy.Threads;
7 | public partial class DummyUserSimulationThread
8 | {
9 | private bool m_LastPrimary;
10 | private bool m_LastSecondary;
11 | private uint m_LastPunch;
12 |
13 | private void SimulateEquipment()
14 | {
15 | if (Player.equipment.HasValidUseable)
16 | {
17 | // todo: simulate useable
18 | return;
19 | }
20 |
21 | SimulatePunch();
22 | }
23 |
24 | private void SimulatePunch()
25 | {
26 | Punch(MouseState.Left, ref m_LastPrimary);
27 | Punch(MouseState.Right, ref m_LastSecondary);
28 | }
29 |
30 | private void Punch(MouseState punch, ref bool lastPunch)
31 | {
32 | var state = MouseState;
33 | var isBusy = Player.equipment.isBusy;
34 |
35 | if (state.HasFlag(punch) != lastPunch)
36 | {
37 | lastPunch = state.HasFlag(punch);
38 | if (!isBusy && Player.stance.stance is not EPlayerStance.PRONE && lastPunch && m_Simulation - m_LastPunch > 5)
39 | {
40 | m_LastPunch = m_Simulation;
41 | SendPunch();
42 | }
43 | }
44 |
45 | void SendPunch()
46 | {
47 | var raycastInfo = DamageTool.raycast(new Ray(Player.look.aim.position, Player.look.aim.forward),
48 | 1.75f, RayMasks.DAMAGE_CLIENT, Player);
49 |
50 | (m_Packet.clientsideInputs ??= new()).Add(new(raycastInfo, ERaycastInfoUsage.Punch));
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Dummy/Threads/DummyUserSimulationThread.Movement.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using SDG.Framework.Water;
3 | using SDG.Unturned;
4 | using UnityEngine;
5 |
6 | namespace Dummy.Threads;
7 |
8 | public partial class DummyUserSimulationThread
9 | {
10 | private Vector3 m_Velocity;
11 | private Vector3 m_OldPosition;
12 |
13 | private float fall => m_Velocity.y;
14 |
15 | private void SimulateAsClient()
16 | {
17 | var movement = Player.movement;
18 | var transform = Player.transform;
19 | var normalizedMove = Move.normalized;
20 | var speed = movement.speed;
21 | var deltaTime = PlayerInput.RATE;
22 | var stance = Player.stance.stance;
23 | var controller = movement.controller;
24 | var aim = Player.look.aim;
25 |
26 | if (Player.input.clientHasPendingResimulation)
27 | {
28 | Player.input.clientHasPendingResimulation = false;
29 |
30 | controller.enabled = false;
31 | transform.position = Player.input.clientResimulationPosition;
32 | controller.enabled = true;
33 |
34 | m_Velocity = Player.input.clientResimulationVelocity;
35 |
36 | m_OldPosition = transform.position;
37 | goto ForceCreatePacket;
38 | }
39 |
40 | m_OldPosition = transform.position;
41 |
42 | switch (stance)
43 | {
44 | case EPlayerStance.SITTING:
45 | break;
46 | case EPlayerStance.CLIMB:
47 | //s_FallProperty.SetValue(movement, c_Jump);
48 | m_Velocity = new(0, Move.z * speed * 0.5f, 0);
49 | controller.CheckedMove(m_Velocity * deltaTime);
50 | break;
51 | case EPlayerStance.SWIM:
52 | if (Player.stance.isSubmerged || (Player.look.pitch > 110 && Move.z > 0.1f))
53 | {
54 | m_Velocity = aim.rotation * normalizedMove * speed * 1.5f;
55 |
56 | if (Jump)
57 | {
58 | m_Velocity.y = c_Swim * movement.pluginJumpMultiplier;
59 | }
60 |
61 | controller.CheckedMove(m_Velocity * deltaTime);
62 | break;
63 | }
64 |
65 | WaterUtility.getUnderwaterInfo(transform.position, out var _, out var surfaceElevation);
66 | m_Velocity = transform.rotation * normalizedMove * speed * 1.5f;
67 | m_Velocity.y = (surfaceElevation - 1.275f - transform.position.y) / 8f;
68 | controller.CheckedMove(m_Velocity * deltaTime);
69 |
70 | break;
71 | default:
72 | var isMovementBlocked = false;
73 | var shouldUpdateVelocity = false;
74 |
75 | movement.checkGround(transform.position);
76 |
77 | if (movement.isGrounded && movement.ground.normal.y > 0)
78 | {
79 | var slopeAngle = Vector3.Angle(Vector3.up, movement.ground.normal);
80 | var maxWalkableSlope = 59f;
81 | if (Level.info?.configData?.Max_Walkable_Slope > -0.5f)
82 | {
83 | maxWalkableSlope = Level.info.configData.Max_Walkable_Slope;
84 | }
85 |
86 | if (slopeAngle > maxWalkableSlope)
87 | {
88 | isMovementBlocked = true;
89 | var a = Vector3.Cross(Vector3.Cross(Vector3.up, movement.ground.normal), movement.ground.normal);
90 | m_Velocity += a * 16f * deltaTime;
91 | shouldUpdateVelocity = true;
92 | }
93 | }
94 |
95 | if (!isMovementBlocked)
96 | {
97 | var moveVector = transform.rotation * (normalizedMove * speed);
98 |
99 | if (movement.isGrounded)
100 | {
101 | var characterFrictionProperties = PhysicMaterialCustomData.GetCharacterFrictionProperties(movement.materialName);
102 | if (characterFrictionProperties.mode == EPhysicsMaterialCharacterFrictionMode.ImmediatelyResponsive)
103 | {
104 | moveVector = Vector3.Cross(Vector3.Cross(Vector3.up, moveVector), movement.ground.normal);
105 | moveVector.y = Mathf.Min(moveVector.y, 0f);
106 | m_Velocity = moveVector;
107 | }
108 | else
109 | {
110 | Vector3 vector3 = Vector3.ProjectOnPlane(m_Velocity, movement.ground.normal);
111 | float magnitude = vector3.magnitude;
112 | Vector3 vector4 = Vector3.Cross(Vector3.Cross(Vector3.up, moveVector), movement.ground.normal);
113 | vector4 *= characterFrictionProperties.maxSpeedMultiplier;
114 | float magnitude2 = vector4.magnitude;
115 | float num7;
116 | if (magnitude > magnitude2)
117 | {
118 | float num6 = -2f * characterFrictionProperties.decelerationMultiplier;
119 | num7 = Mathf.Max(magnitude2, magnitude + (num6 * deltaTime));
120 | }
121 | else
122 | {
123 | num7 = magnitude2;
124 | }
125 | Vector3 vector5 = vector4 * characterFrictionProperties.accelerationMultiplier;
126 | Vector3 vector6 = vector3 + (vector5 * deltaTime);
127 | m_Velocity = vector6.ClampMagnitude(num7);
128 | shouldUpdateVelocity = true;
129 | }
130 | }
131 | else
132 | {
133 | m_Velocity.y += Physics.gravity.y * ((fall <= 0f) ? movement.totalGravityMultiplier : 1f) * deltaTime * 3f;
134 | var maxFall = (movement.totalGravityMultiplier < 0.99f) ? (Physics.gravity.y * 2f * movement.totalGravityMultiplier) : (-100f);
135 | m_Velocity.y = Mathf.Max(maxFall, m_Velocity.y);
136 |
137 | var moveHorizontalMagnitude = moveVector.GetHorizontalMagnitude();
138 | var horizontalVelocity = m_Velocity.GetHorizontal();
139 | var horizontalMagnitude2 = m_Velocity.GetHorizontalMagnitude();
140 | float maxMagnitude;
141 | if (horizontalMagnitude2 > moveHorizontalMagnitude)
142 | {
143 | var num5 = 2f * Provider.modeConfigData.Gameplay.AirStrafing_Deceleration_Multiplier;
144 | maxMagnitude = Mathf.Max(moveHorizontalMagnitude, horizontalMagnitude2 - (num5 * deltaTime));
145 | }
146 | else
147 | {
148 | maxMagnitude = moveHorizontalMagnitude;
149 | }
150 |
151 | var a3 = moveVector * (4f * Provider.modeConfigData.Gameplay.AirStrafing_Acceleration_Multiplier);
152 | var vector2 = horizontalVelocity + (a3 * deltaTime);
153 | vector2 = vector2.ClampHorizontalMagnitude(maxMagnitude);
154 | m_Velocity.x = vector2.x;
155 | m_Velocity.z = vector2.z;
156 | shouldUpdateVelocity = true;
157 | }
158 | }
159 |
160 | var jumpMastery = Player.skills.mastery(0, 6);
161 | if (Jump
162 | && movement.isGrounded
163 | && !Player.life.isBroken
164 | && Player.life.stamina >= 10f * (1f - (jumpMastery * 0.5f))
165 | && stance is EPlayerStance.STAND or EPlayerStance.SPRINT
166 | && !MathfEx.IsNearlyZero(movement.pluginJumpMultiplier, 0.001f))
167 | {
168 | m_Velocity.y = c_Jump * (1f + (jumpMastery * 0.25f)) * movement.pluginJumpMultiplier;
169 | }
170 |
171 | m_Velocity += movement.pendingLaunchVelocity;
172 | movement.pendingLaunchVelocity = Vector3.zero;
173 |
174 | var previousPosition = movement.transform.position;
175 | controller.CheckedMove(m_Velocity * deltaTime);
176 |
177 | if (shouldUpdateVelocity)
178 | {
179 | m_Velocity = (transform.position - previousPosition) / deltaTime;
180 | }
181 |
182 | break;
183 | }
184 |
185 | ForceCreatePacket:
186 | var x = (int)Move.x;
187 | var z = (int)Move.z;
188 |
189 | var horizontal = (byte)(x + 1);
190 | var vertical = (byte)(z + 1);
191 |
192 | m_Packet = new WalkingPlayerInputPacket
193 | {
194 | analog = (byte)((horizontal << 4) | vertical),
195 | clientPosition = transform.position,
196 | pitch = m_Pitch,
197 | yaw = m_Yaw
198 | };
199 |
200 | m_Packet.clientSimulationFrameNumber = m_Simulation;
201 | m_Packet.recov = Player.input.recov;
202 |
203 | m_Simulation++;
204 | m_Buffer += PlayerInput.SAMPLES;
205 |
206 | transform.position = m_OldPosition;
207 | }
208 | }
--------------------------------------------------------------------------------
/Dummy/Threads/DummyUserSimulationThread.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using System.Threading.Tasks;
5 | using Cysharp.Threading.Tasks;
6 | using Dummy.Actions.Interaction;
7 | using Dummy.Patches;
8 | using Dummy.Users;
9 | using SDG.Framework.Water;
10 | using SDG.NetPak;
11 | using SDG.Unturned;
12 | using UnityEngine;
13 | using ILogger = Microsoft.Extensions.Logging.ILogger;
14 |
15 | namespace Dummy.Threads
16 | {
17 | public partial class DummyUserSimulationThread : IAsyncDisposable
18 | {
19 | private const float c_Swim = 3f;
20 | private const float c_Jump = 7f;
21 | private const float c_MinAngleSit = 60f;
22 | private const float c_MaxAngleSit = 120f;
23 | private const float c_MinAngleClimb = 45f;
24 | private const float c_MaxAngleClimb = 100f;
25 | private const float c_MinAngleSwim = 45f;
26 | private const float c_MaxAngleSwim = 135f;
27 | private const float c_MinAngleStand = 0f;
28 | private const float c_MaxAngleStand = 180f;
29 | private const float c_MinAngleCrouch = 20f;
30 | private const float c_MaxAngleCrouch = 160f;
31 | private const float c_MinAngleProne = 60f;
32 | private const float c_MaxAngleProne = 120f;
33 |
34 | private readonly bool[] m_Keys;
35 | private readonly DummyUser m_PlayerDummy;
36 | private readonly ILogger m_Logger;
37 |
38 | private PlayerInputPacket m_Packet;
39 | private uint m_Count;
40 | private uint m_Buffer;
41 | private uint m_Consumed;
42 | private uint m_Simulation;
43 | private float m_Yaw;
44 | private float m_Pitch;
45 | private float m_TimeLerp;
46 |
47 | private Player Player => m_PlayerDummy.Player.Player;
48 |
49 | public bool Enabled { get; set; }
50 |
51 | ///
52 | /// Normalized move direction [-1 ; 1]. The Y direction should be always 0.
53 | ///
54 | public Vector3 Move { get; set; }
55 |
56 | public bool Jump // will be jumping until consume all stamina
57 | {
58 | get => m_Keys[0];
59 | set => m_Keys[0] = value;
60 | }
61 |
62 | public MouseState MouseState
63 | {
64 | get
65 | {
66 | var left = m_Keys[1] ? 1 : 0;
67 | var right = m_Keys[2] ? 2 : 0;
68 |
69 | return (MouseState)(left + right);
70 | }
71 | set
72 | {
73 | if (value is > MouseState.LeftRight or < MouseState.None)
74 | {
75 | throw new ArgumentOutOfRangeException(nameof(MouseState));
76 | }
77 |
78 | var left = (value & MouseState.Left) != 0;
79 | var right = (value & MouseState.Right) != 0;
80 |
81 | m_Keys[1] = left;
82 | m_Keys[2] = right;
83 | }
84 | }
85 |
86 | public bool Crouch
87 | {
88 | get => m_Keys[3];
89 | set => m_Keys[3] = value;
90 | }
91 |
92 | public bool Prone
93 | {
94 | get => m_Keys[4];
95 | set => m_Keys[4] = value;
96 | }
97 |
98 | public bool Sprint // will be sprinting until consume all stamina
99 | {
100 | get => m_Keys[5];
101 | set => m_Keys[5] = value;
102 | }
103 |
104 | public bool LeanLeft
105 | {
106 | get => m_Keys[6];
107 | set => m_Keys[6] = value;
108 | }
109 |
110 | public bool LeanRight
111 | {
112 | get => m_Keys[7];
113 | set => m_Keys[7] = value;
114 | }
115 |
116 | public bool PluginKey1
117 | {
118 | get => m_Keys[10];
119 | set => m_Keys[10] = value;
120 | }
121 |
122 | public bool PluginKey2
123 | {
124 | get => m_Keys[11];
125 | set => m_Keys[11] = value;
126 | }
127 |
128 | public bool PluginKey3
129 | {
130 | get => m_Keys[12];
131 | set => m_Keys[12] = value;
132 | }
133 |
134 | public bool PluginKey4
135 | {
136 | get => m_Keys[13];
137 | set => m_Keys[13] = value;
138 | }
139 |
140 | public bool PluginKey5
141 | {
142 | get => m_Keys[14];
143 | set => m_Keys[14] = value;
144 | }
145 |
146 | public DummyUserSimulationThread(DummyUser playerDummy, ILogger logger)
147 | {
148 | m_PlayerDummy = playerDummy;
149 | m_Logger = logger;
150 |
151 | m_Count = 0;
152 | m_Buffer = 0;
153 | m_Consumed = 0;
154 | m_Pitch = 90f;
155 | Move = Vector3.zero;
156 | m_Packet = new();
157 |
158 | var countKeys = 10 + ControlsSettings.NUM_PLUGIN_KEYS;
159 | m_Keys = new bool[countKeys];
160 |
161 | Player.onPlayerTeleported += OnPlayerTeleported;
162 | }
163 |
164 | private void OnPlayerTeleported(Player player, Vector3 point)
165 | {
166 | m_OldPosition = point;
167 |
168 | /*if (m_Packet is WalkingPlayerInputPacket walking)
169 | {
170 | walking.clientPosition = point;
171 | }*/
172 | }
173 |
174 | public void SetRotation(float yaw, float pitch, float time)
175 | {
176 | if (!yaw.IsFinite())
177 | {
178 | yaw = 0;
179 | }
180 |
181 | if (!pitch.IsFinite())
182 | {
183 | pitch = 0;
184 | }
185 |
186 | if (!time.IsFinite() || time <= 0)
187 | {
188 | time = 1f;
189 | }
190 |
191 | m_Yaw = yaw;
192 | m_Pitch = pitch;
193 | m_TimeLerp = time;
194 |
195 | ClampPitch();
196 | ClampYaw();
197 | }
198 |
199 | /* Simulate (0) [m_Count % PlayerInput.SAMPLES == 0]
200 | * 3 FU
201 | * Send (3) [m_Consumed == m_Buffer]
202 | * Simulate (4)
203 | * 3 FU
204 | * Send (7)
205 | * ...
206 | */
207 |
208 | public async UniTaskVoid Start()
209 | {
210 | await UniTask.DelayFrame(1, PlayerLoopTiming.FixedUpdate);
211 | if (Player == null)
212 | {
213 | return;
214 | }
215 |
216 | if (Player.transform.TryGetComponent(out var rigidbody))
217 | {
218 | UnityEngine.Object.Destroy(rigidbody);
219 | }
220 |
221 | var queue = Player.input.serversidePackets;
222 |
223 | while (Enabled)
224 | {
225 | // Do not simulate if dead
226 | if (Player.life.isDead)
227 | goto Exit;
228 |
229 | if (m_Count % PlayerInput.SAMPLES == 0)
230 | {
231 | await m_PlayerDummy.Actions.ExecuteActions();
232 |
233 | SimulateAsClient();
234 | SimulateEquipment();
235 | }
236 |
237 | if (m_Consumed < m_Buffer)
238 | {
239 | m_Consumed++;
240 | }
241 |
242 | if (m_Consumed == m_Buffer)
243 | {
244 | ushort compressedKeys = 0;
245 | for (var b = 0; b < m_Keys.Length; b++)
246 | {
247 | if (m_Keys[b])
248 | compressedKeys |= Player.input.flags[b];
249 | }
250 | m_Packet.keys = compressedKeys;
251 |
252 | var netWriter = NetMessages.GetInvokableWriter();
253 | netWriter.Reset();
254 | m_Packet.write(netWriter);
255 | netWriter.Flush();
256 |
257 | var netRead = NetMessages.GetInvokableReader();
258 | netRead.SetBufferSegment(netWriter.buffer, netWriter.writeByteIndex);
259 | netRead.Reset();
260 |
261 | m_Packet.read(Player.channel, netRead);
262 |
263 | queue.Enqueue(m_Packet);
264 | }
265 |
266 | Exit:
267 | // simulate dummy ping
268 | if (m_Count % 50 == 0)
269 | {
270 | Player.channel.owner.lag(1000f / Provider.debugUPS / 1000f);
271 | }
272 |
273 | m_Count++;
274 | await UniTask.WaitForFixedUpdate();
275 | }
276 | }
277 |
278 | private void ClampPitch()
279 | {
280 | var vehicleSeat = Player.movement.getVehicleSeat();
281 | var min = 0f;
282 | var max = 180f;
283 |
284 | if (vehicleSeat != null)
285 | {
286 | if (vehicleSeat.turret != null)
287 | {
288 | min = vehicleSeat.turret.pitchMin;
289 | max = vehicleSeat.turret.pitchMax;
290 | }
291 | else
292 | {
293 | min = c_MinAngleSit;
294 | max = c_MaxAngleSit;
295 | }
296 | }
297 | else
298 | {
299 | switch (Player.stance.stance)
300 | {
301 | case EPlayerStance.STAND or EPlayerStance.SPRINT:
302 | min = c_MinAngleStand;
303 | max = c_MaxAngleStand;
304 | break;
305 | case EPlayerStance.CLIMB:
306 | min = c_MinAngleClimb;
307 | max = c_MaxAngleClimb;
308 | break;
309 | case EPlayerStance.SWIM:
310 | min = c_MinAngleSwim;
311 | max = c_MaxAngleSwim;
312 | break;
313 | case EPlayerStance.CROUCH:
314 | min = c_MinAngleCrouch;
315 | max = c_MaxAngleCrouch;
316 | break;
317 | case EPlayerStance.PRONE:
318 | min = c_MinAngleProne;
319 | max = c_MaxAngleProne;
320 | break;
321 | }
322 | }
323 |
324 | m_Pitch = Mathf.Clamp(m_Pitch, min, max);
325 | }
326 |
327 | private void ClampYaw()
328 | {
329 | m_Yaw %= 360f;
330 | var vehicleSeat = Player.movement.getVehicleSeat();
331 | if (vehicleSeat == null)
332 | {
333 | return;
334 | }
335 |
336 | var min = -90f;
337 | var max = 90f;
338 | if (vehicleSeat.turret != null)
339 | {
340 | min = vehicleSeat.turret.yawMin;
341 | max = vehicleSeat.turret.yawMax;
342 | }
343 | else if (Player.stance.stance == EPlayerStance.DRIVING)
344 | {
345 | min = -160f;
346 | max = 160f;
347 | }
348 |
349 | m_Yaw = Mathf.Clamp(m_Yaw, min, max);
350 | }
351 |
352 | public ValueTask DisposeAsync()
353 | {
354 | Enabled = false;
355 |
356 | Player.onPlayerTeleported -= OnPlayerTeleported;
357 | return new();
358 | }
359 | }
360 | }
--------------------------------------------------------------------------------
/Dummy/Users/DummyUser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Cysharp.Threading.Tasks;
5 | using Dummy.Players;
6 | using Dummy.Threads;
7 | using Microsoft.Extensions.Localization;
8 | using Microsoft.Extensions.Logging;
9 | using OpenMod.API.Users;
10 | using OpenMod.Core.Users;
11 | using OpenMod.Extensions.Games.Abstractions.Players;
12 | using OpenMod.UnityEngine.Extensions;
13 | using OpenMod.Unturned.Users;
14 | using SDG.Unturned;
15 | using Steamworks;
16 | using UnityEngine;
17 | using Color = System.Drawing.Color;
18 |
19 | namespace Dummy.Users
20 | {
21 | public class DummyUser : UnturnedUser, IPlayerUser, IAsyncDisposable, IEquatable
22 | {
23 | private readonly IStringLocalizer m_StringLocalizer;
24 |
25 | public DummyUserActionThread Actions { get; }
26 | public DummyUserSimulationThread Simulation { get; }
27 | public new DummyPlayer Player { get; }
28 | public HashSet Owners { get; }
29 |
30 | public CSteamID SteamID => Player.SteamId;
31 | public SteamPlayer SteamPlayer => Player.SteamPlayer;
32 |
33 | IPlayer IPlayerUser.Player => Player;
34 |
35 | public DummyUser(UnturnedUserProvider userProvider, IUserDataStore userDataStore, SteamPlayer steamPlayer,
36 | ILoggerFactory loggerFactory, IStringLocalizer stringLocalizer, bool disableSimulation,
37 | HashSet? owners = null)
38 | : base(userProvider, userDataStore, steamPlayer.player)
39 | {
40 | Player = new(steamPlayer);
41 | m_StringLocalizer = stringLocalizer;
42 | Owners = owners ?? new HashSet();
43 | Actions = new(this, loggerFactory.CreateLogger($"Dummy.{Id}.Action"));
44 | Simulation = new(this, loggerFactory.CreateLogger($"Dummy.{Id}.Simulation"));
45 |
46 | Actions.Enabled = true;
47 | Simulation.Enabled = !disableSimulation;
48 |
49 | Simulation.Start().Forget();
50 | }
51 |
52 | public override Task PrintMessageAsync(string message)
53 | {
54 | return PrintMessageAsync(message, Color.White, true, null);
55 | }
56 |
57 | public override Task PrintMessageAsync(string message, Color color)
58 | {
59 | return PrintMessageAsync(message, color, true, null);
60 | }
61 |
62 | private new Task PrintMessageAsync(string message, Color color, bool isRich, string? iconUrl)
63 | {
64 | async UniTask PrintMessageTask()
65 | {
66 | await UniTask.SwitchToMainThread();
67 |
68 | foreach (var owner in Owners)
69 | {
70 | var player = PlayerTool.getPlayer(owner);
71 | if (player == null)
72 | {
73 | continue;
74 | }
75 |
76 | ChatManager.serverSendMessage(m_StringLocalizer["events:chatted", new { Id, Text = message }],
77 | color.ToUnityColor(),
78 | toPlayer: player.channel.owner, iconURL: iconUrl, useRichTextFormatting: isRich);
79 | }
80 | }
81 |
82 | return PrintMessageTask().AsTask();
83 | }
84 |
85 | public bool Equals(DummyUser other)
86 | {
87 | if (other is null)
88 | return false;
89 | if (ReferenceEquals(this, other))
90 | return true;
91 | return other.SteamID.Equals(SteamID);
92 | }
93 |
94 | public override bool Equals(object obj)
95 | {
96 | return Equals((obj as DummyUser)!);
97 | }
98 |
99 | public override int GetHashCode()
100 | {
101 | return Player.GetHashCode();
102 | }
103 |
104 | public async ValueTask DisposeAsync()
105 | {
106 | await Simulation.DisposeAsync();
107 | await Actions.DisposeAsync();
108 |
109 | if (Session == null)
110 | {
111 | return;
112 | }
113 |
114 | if (Session is UnturnedUserSession session)
115 | {
116 | session.OnSessionEnd();
117 | }
118 |
119 | await Session.DisconnectAsync();
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/Dummy/config.yaml:
--------------------------------------------------------------------------------
1 | options:
2 | # Max dummies can be spawned at the same time
3 | amountDummies: 5
4 | # On spawning, a dummy will be an admin
5 | isAdmin: false
6 | # Enabling this a dummy can execute commands without admin or permission.
7 | canExecuteCommands: true
8 | # [WARNING] Disabling this option can break plugin but if your host-server is slow then disable (takes about ~2-5% of CPU each a dummy)
9 | disableSimulations: false
10 |
11 | # Configuration for events (helpful for developers)
12 | # With those events, you can check if a dummy can enter to the server (like a real player)
13 | events:
14 | # Enabling this dummy can be damaged.
15 | allowDamage: true
16 | # Before entering the server will be called an event Provider.onCheckValidWithExplanation
17 | callOnCheckValidWithExplanation: true
18 | # Before entering the server will be called an event Provider.onCheckBanStatusWithHWID
19 | callOnCheckBanStatusWithHWID: true
20 |
21 | connection:
22 | # On a new connection, a dummy will create the ip and port.
23 | # If it set to false then it's will use ip / port from the default section
24 | randomIp: true
25 | randomPort: true
26 |
27 | # Just for fun :D
28 | fun:
29 | # Dummy will be always rotating (each 1ms rotate X°)
30 | alwaysRotate: true
31 | # The X deg.
32 | rotateYaw: 10
33 |
34 | # On spawning dummy (/dummy spawn) it will use default configuration
35 | default:
36 | isPro: true
37 | characterId: 0
38 | playerName: dummy
39 | characterName: dummy
40 | nickName: dummy
41 | ip: 192.168.0.1
42 | port: 25565
43 | hwid: 13FA0C5BD7554B2CBDB2AEAC8D9B06B8
44 | steamGroupId: 0
45 | skinColor: "#94764B"
46 | beardId: 5
47 | beardColor: "#FFFFFF"
48 | hairId: 9
49 | hairColor: "#FFFFFF"
50 | faceId: 31
51 | markerColor: "Orange"
52 | isLeftHanded: false
53 | # ID can be get in the EconInfo.json in the U3DS or Unturned root folder.
54 | # (item_id)
55 | skins:
56 | shirt: 553
57 | pants: 902
58 | hat: 555
59 | backpack: 895
60 | vest: 898
61 | mask: 737
62 | glasses: 491
63 | # Available skillsets:
64 | # NONE, FIRE, POLICE, ARMY, FARM, FISH, CAMP, WORK, CHEF, THIEF, MEDIC
65 | playerSkillset: POLICE
66 | language: English
67 | lobbyId: 0
68 |
69 | logs:
70 | # Log in the chat when dummy received a message from someone
71 | enableChatReceiveLog: true
72 | # Log in the chat when dummy takes damage
73 | enableDamageLog: true
74 | # Log in the chat when dummy died
75 | enableDeathLog: true
76 | # Log in console when dummy spawns
77 | enableJoinLeaveLog: false
--------------------------------------------------------------------------------
/Dummy/translations.yaml:
--------------------------------------------------------------------------------
1 | commands:
2 | dummyNotFound: "Dummy with id ({Id}) not found"
3 | actions:
4 | button:
5 | success: "Dummy {Id} clicked to {ButtonName} button"
6 | execute:
7 | success: "Dummy {Id} successfully executed a command: {Command}"
8 | fail: "Dummy {Id} failed executed a command: {Command}"
9 | face:
10 | success: "Dummy {Id} changed a face {FaceIndex}"
11 | gesture:
12 | success: "Dummy {Id} gestures a {EGesture}"
13 | inputText:
14 | success: "Dummy {Id} entered a text {Text} to input field {InputFieldName}"
15 | stance:
16 | success: "Dummy {Id} stances a {EStance}"
17 | general:
18 | clear: "Dummies have been cleared"
19 | create: "Dummy {Id} has been created"
20 | copy: "Dummy {Id} has been created"
21 | remove:
22 | success: "Dummy {Id} has been removed"
23 | fail: "Dummy {Id} has not been removed"
24 | tphere:
25 | success: "Dummy {Id} teleported to you"
26 | fail: "Dummy {Id} not teleported to you"
27 |
28 | events:
29 | dead: "Dummy {Id} has been died. Reason: {Reason}, killer = {Killer}. Respawning..."
30 | revived: "Dummy {Id} has been revived"
31 | damaged: "Dummy {Id} has been damaged {DamageAmount}"
32 | chatted: "Dummy {Id} got message: {Text}"
33 |
34 | exceptions:
35 | contains: "Dummy with id: {Id} already created"
36 | overflow: "Can't create a dummy. Max dummies reach limit: {DummiesCount}/{MaxDummies}"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 DiFFoZ
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dummy
2 | Dummy is a plugin for Unturned / OpenMod. It spawns a dummy and shows the amount of damage when damaged. Also, very helpful with debugging plugins.
3 |
4 | [](https://www.nuget.org/packages/EvolutionPlugins.Dummy/)
5 | [](https://www.nuget.org/packages/EvolutionPlugins.Dummy/)
6 | [](https://discord.gg/5MT2yke)
7 |
8 | # Commands
9 | _Maybe outdated so check help.md to get all commands_
10 | - /dummy create - Creates a dummy with unique ID.
11 |
12 | - /dummy copy - Creates a dummy with unique ID and copy skin, clothes, beard, hair, face from an owner.
13 |
14 | - /dummy remove <id> - Remove dummy by ID.
15 |
16 | - /dummy clear - Clear all dummies.
17 |
18 | - /dummy execute <id> <command> - Execute a command.
19 |
20 | - /dummy tphere <id> - Teleport to you a dummy.
21 |
22 | - /dummy gesture <id> <gesture> - Send gesture to a dummy. Gesture can be found [here.](https://github.com/EvolutionPlugins/Dummy#gestures)
23 |
24 | - /dummy stance <id> <stance> - Send stance to a dummy. Stance can be found [here.](https://github.com/EvolutionPlugins/Dummy#stances)
25 |
26 | - /dummy face <id> <faceIndex> - Send face to a dummy. FaceIndex can be found [here.](https://github.com/EvolutionPlugins/Dummy#index-of-faces)
27 |
28 | - /dummy button <id> <buttonName> - Click to button.
29 |
30 | - /dummy inputfield <id> <inputFieldName> <Text> - Input text in *InputField*
31 |
32 | # Permissions
33 | _Maybe outdated so check help.md to get all permissions
34 | - EvolutionPlugins.Dummy:commands.dummy
35 | - EvolutionPlugins.Dummy:commands.dummy.clear
36 | - EvolutionPlugins.Dummy:commands.dummy.copy
37 | - EvolutionPlugins.Dummy:commands.dummy.create
38 | - EvolutionPlugins.Dummy:commands.dummy.remove
39 | - EvolutionPlugins.Dummy:commands.dummy.tphere
40 | - EvolutionPlugins.Dummy:commands.dummy.button
41 | - EvolutionPlugins.Dummy:commands.dummy.execute
42 | - EvolutionPlugins.Dummy:commands.dummy.face
43 | - EvolutionPlugins.Dummy:commands.dummy.gesture
44 | - EvolutionPlugins.Dummy:commands.dummy.inputfield
45 | - EvolutionPlugins.Dummy:commands.dummy.jump
46 | - EvolutionPlugins.Dummy:commands.dummy.stance
47 |
48 | # Configuration
49 | _Outdated_
50 | - amountDummies _( default: 1 )_ - Max dummies in same time.
51 |
52 | - kickDummyAfterSeconds _( default: 300 )_ - Kick automatically dummy after amount of seconds.
53 |
54 | - isAdmin _(default: false)_ - On spawning make a dummy admin
55 |
56 | # Tips
57 |
58 | ## Gestures
59 |
60 | | Gesture | Index |
61 | |:---------------: |:-----: |
62 | | INVENTORY_START | 1 |
63 | | INVENTORY_STOP | 2 |
64 | | PICKUP | 3 |
65 | | PUNCH_LEFT | 4 |
66 | | PUNCH_RIGHT | 5 |
67 | | SURRENDER_START | 6 |
68 | | SURRENDER_STOP | 7 |
69 | | POINT | 8 |
70 | | WAVE | 9 |
71 | | SALUTE | 10 |
72 | | ARREST_START | 11 |
73 | | ARREST_STOP | 12 |
74 | | REST_START | 13 |
75 | | REST_STOP | 14 |
76 | | FACEPALM | 15 |
77 |
78 | ## Stances
79 |
80 | | Stance | Index |
81 | |:-------: |:-----: |
82 | | CLIMB | 0 |
83 | | SWIM | 1 |
84 | | SPRINT | 2 |
85 | | STAND | 3 |
86 | | CROUCH | 4 |
87 | | PRONE | 5 |
88 | | DRIVING | 6 |
89 | | SITTING | 7 |
90 |
91 | ## Index of faces
92 |
93 | 
94 |
95 | ## Index of beards/hairs
96 |
97 | 
98 |
99 | # Where I can get support
100 | You can get support in my [discord server](https://discord.gg/5MT2yke)
101 |
--------------------------------------------------------------------------------