├── .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 | [![Nuget](https://img.shields.io/nuget/v/EvolutionPlugins.Dummy)](https://www.nuget.org/packages/EvolutionPlugins.Dummy/) 5 | [![Nuget](https://img.shields.io/nuget/dt/EvolutionPlugins.Dummy?label=nuget%20downloads)](https://www.nuget.org/packages/EvolutionPlugins.Dummy/) 6 | [![Discord](https://img.shields.io/discord/764502843906064434?label=Discord%20chat)](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 | ![face](https://i.redd.it/eqve40c3cuqx.png) 94 | 95 | ## Index of beards/hairs 96 | 97 | ![image1](https://i.redd.it/t9qh0q76l16z.jpg) 98 | 99 | # Where I can get support 100 | You can get support in my [discord server](https://discord.gg/5MT2yke) 101 | --------------------------------------------------------------------------------