├── .gitattributes ├── .gitignore ├── .gitmodules ├── GhostMod.sln ├── GhostMod ├── Content │ └── Dialog │ │ └── English.txt ├── Ghost.cs ├── GhostChunkData.cs ├── GhostChunkInput.cs ├── GhostData.cs ├── GhostExtensions.cs ├── GhostFrame.cs ├── GhostInputNodes.cs ├── GhostInputReplayer.cs ├── GhostManager.cs ├── GhostMod.csproj ├── GhostModule.cs ├── GhostModuleSettings.cs ├── GhostName.cs ├── GhostRecorder.cs └── Properties │ └── AssemblyInfo.cs ├── GhostNetMod ├── Chunks │ ├── ChunkEAudioState.cs │ ├── ChunkEAudioTrackState.cs │ ├── ChunkHHead.cs │ ├── ChunkMChat.cs │ ├── ChunkMEmote.cs │ ├── ChunkMPlayer.cs │ ├── ChunkMRequest.cs │ ├── ChunkMServerInfo.cs │ ├── ChunkMSession.cs │ ├── ChunkRListAreas.cs │ ├── ChunkRListMods.cs │ ├── ChunkUActionCollision.cs │ ├── ChunkUAudioPlay.cs │ ├── ChunkUParticles.cs │ ├── ChunkUUpdate.cs │ └── IChunk.cs ├── Connection │ ├── GhostNetConnection.cs │ ├── GhostNetLocalConnection.cs │ └── GhostNetRemoteConnection.cs ├── Content │ ├── Dialog │ │ └── English.txt │ └── Graphics │ │ └── Atlases │ │ └── Gui │ │ └── ghostnetmod │ │ └── iconwheel │ │ ├── bg.png │ │ ├── indicator.png │ │ └── line.png ├── GhostNetClient.cs ├── GhostNetCommand.cs ├── GhostNetCommandsStandard.cs ├── GhostNetEmote.cs ├── GhostNetEmoteWheel.cs ├── GhostNetExtensions.cs ├── GhostNetFrame.cs ├── GhostNetHooks.cs ├── GhostNetMod.csproj ├── GhostNetModule.cs ├── GhostNetModuleSettings.cs ├── GhostNetParticleHelper.cs ├── GhostNetRaceManager.cs ├── GhostNetServer.cs ├── GhostNetWatchdog.cs └── Properties │ └── AssemblyInfo.cs ├── LICENSE ├── README.md ├── deps ├── MMHOOK_Celeste.dll ├── System.Buffers.dll ├── System.Memory.dll ├── System.Runtime.CompilerServices.Unsafe.dll ├── Tmds.Systemd.dll └── Tmds.Systemd.xml └── everest.yaml /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Windows Store app package directory 170 | AppPackages/ 171 | BundleArtifacts/ 172 | 173 | # Visual Studio cache files 174 | # files ending in .cache can be ignored 175 | *.[Cc]ache 176 | # but keep track of directories ending in .cache 177 | !*.[Cc]ache/ 178 | 179 | # Others 180 | ClientBin/ 181 | [Ss]tyle[Cc]op.* 182 | ~$* 183 | *~ 184 | *.dbmdl 185 | *.dbproj.schemaview 186 | *.pfx 187 | *.publishsettings 188 | node_modules/ 189 | orleans.codegen.cs 190 | 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | 194 | # Backup & report files from converting an old project file 195 | # to a newer Visual Studio version. Backup files are not needed, 196 | # because we have git ;-) 197 | _UpgradeReport_Files/ 198 | Backup*/ 199 | UpgradeLog*.XML 200 | UpgradeLog*.htm 201 | 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | 214 | # GhostDoc plugin setting file 215 | *.GhostDoc.xml 216 | 217 | # Node.js Tools for Visual Studio 218 | .ntvs_analysis.dat 219 | 220 | # Visual Studio 6 build log 221 | *.plg 222 | 223 | # Visual Studio 6 workspace options file 224 | *.opt 225 | 226 | # Visual Studio LightSwitch build output 227 | **/*.HTMLClient/GeneratedArtifacts 228 | **/*.DesktopClient/GeneratedArtifacts 229 | **/*.DesktopClient/ModelManifest.xml 230 | **/*.Server/GeneratedArtifacts 231 | **/*.Server/ModelManifest.xml 232 | _Pvt_Extensions 233 | 234 | # LightSwitch generated files 235 | GeneratedArtifacts/ 236 | ModelManifest.xml 237 | 238 | # Paket dependency manager 239 | .paket/paket.exe 240 | 241 | # FAKE - F# Make 242 | .fake/ 243 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Everest"] 2 | path = Everest 3 | url = https://github.com/EverestAPI/Everest.git 4 | -------------------------------------------------------------------------------- /GhostMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GhostMod", "GhostMod\GhostMod.csproj", "{4ABF8C07-C533-407E-9EC9-534C2916C907}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Celeste.Mod.mm", "Everest\Celeste.Mod.mm\Celeste.Mod.mm.csproj", "{D5D0239D-FF95-4897-9484-1898AB7E82F5}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GhostNetMod", "GhostNetMod\GhostNetMod.csproj", "{2F04DF2A-9512-41A4-B981-2B86A31E4E45}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {4ABF8C07-C533-407E-9EC9-534C2916C907}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {4ABF8C07-C533-407E-9EC9-534C2916C907}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {4ABF8C07-C533-407E-9EC9-534C2916C907}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {4ABF8C07-C533-407E-9EC9-534C2916C907}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {D5D0239D-FF95-4897-9484-1898AB7E82F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D5D0239D-FF95-4897-9484-1898AB7E82F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D5D0239D-FF95-4897-9484-1898AB7E82F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D5D0239D-FF95-4897-9484-1898AB7E82F5}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {2F04DF2A-9512-41A4-B981-2B86A31E4E45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {2F04DF2A-9512-41A4-B981-2B86A31E4E45}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {2F04DF2A-9512-41A4-B981-2B86A31E4E45}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {2F04DF2A-9512-41A4-B981-2B86A31E4E45}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /GhostMod/Content/Dialog/English.txt: -------------------------------------------------------------------------------- 1 | # NOTES: 2 | # The # Symbol at the start of a line counts as a Comment. To include in dialog, use a \# 3 | # The . Symbol will cause a pause unless escaped with \. (ex: Mr. Oshiro has a pause, Mr\. Oshiro does not) 4 | # Newlines automatically create a Page Break, unless there is an {n} command on the previous line 5 | # Commands: Anything inside of curly braces {...} is a command and should not be translated. 6 | 7 | # Inline Text Commands: 8 | # {~}wavy text{/~} 9 | # {!}impact text{/!} 10 | # {>> x}changes speed at which characters are displayed{>>} 11 | # {# 000000}this text is black{#} (uses HEX color values) 12 | # {+MENU_BEGIN} inserts the dialog from the MENU_BEGIN value (in English, "CLIMB") 13 | # {n} creates a newline, without a page break 14 | # {0.5} creates a 0.5 second pause 15 | # {big}this text is large{/big} 16 | 17 | # Gameplay Control Commands (should never change) 18 | # {trigger x} this triggers an in-game event 19 | # {anchor} controls the visual position of the textbox in-game 20 | 21 | # Ghost Module Options 22 | MODOPTIONS_GHOSTMODULE_TITLE= Ghost-eline 23 | MODOPTIONS_GHOSTMODULE_OVERRIDDEN= Ghost-eline - SETTINGS OVERRIDDEN 24 | MODOPTIONS_GHOSTMODULE_MODE= Mode 25 | MODOPTIONS_GHOSTMODULE_MODE_OFF= OFF 26 | MODOPTIONS_GHOSTMODULE_MODE_RECORD= RECORD 27 | MODOPTIONS_GHOSTMODULE_MODE_PLAY= PLAY 28 | MODOPTIONS_GHOSTMODULE_MODE_ON= BOTH 29 | MODOPTIONS_GHOSTMODULE_NAME= Ghost Name 30 | MODOPTIONS_GHOSTMODULE_NAMEFILTER= Filter By Name 31 | MODOPTIONS_GHOSTMODULE_SHOWNAMES= Show Names 32 | MODOPTIONS_GHOSTMODULE_INNEROPACITY= Near Ghost Visibility 33 | MODOPTIONS_GHOSTMODULE_INNERHAIROPACITY= Near Ghost Hair Visibility 34 | MODOPTIONS_GHOSTMODULE_OUTEROPACITY= Far Ghost Visibility 35 | MODOPTIONS_GHOSTMODULE_OUTERHAIROPACITY= Far Ghost Hair Visibility 36 | MODOPTIONS_GHOSTMODULE_INNERRADIUS= Near Ghost Radius 37 | MODOPTIONS_GHOSTMODULE_BORDERSIZE= Gradient Region 38 | MODOPTIONS_GHOSTMODULE_SHOWDEATHS= Show Recorded Deaths 39 | MODOPTIONS_GHOSTMODULE_GHOSTCOUNT= Ghosts 40 | -------------------------------------------------------------------------------- /GhostMod/Ghost.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | public class Ghost : Actor { 14 | 15 | public GhostManager Manager; 16 | 17 | public Player Player; 18 | 19 | public PlayerSprite Sprite; 20 | public PlayerHair Hair; 21 | public int MachineState; 22 | 23 | public GhostData Data; 24 | public int FrameIndex = 0; 25 | public GhostFrame? ForcedFrame; 26 | public GhostFrame PrevFrame => ForcedFrame ?? (Data == null ? default(GhostFrame) : Data[FrameIndex - 1]); 27 | public GhostFrame Frame => ForcedFrame ?? (Data == null ? default(GhostFrame) : Data[FrameIndex]); 28 | public bool AutoForward = true; 29 | 30 | public GhostName Name; 31 | 32 | public Color Color = Color.White; 33 | 34 | protected float alpha; 35 | protected float alphaHair; 36 | 37 | public Ghost(Player player) 38 | : this(player, null) { 39 | } 40 | public Ghost(Player player, GhostData data) 41 | : base(player.Position) { 42 | Player = player; 43 | Data = data; 44 | 45 | Depth = 1; 46 | 47 | Sprite = new PlayerSprite(player.Sprite.Mode); 48 | Sprite.HairCount = player.Sprite.HairCount; 49 | Add(Hair = new PlayerHair(Sprite)); 50 | Add(Sprite); 51 | 52 | Hair.Color = Player.NormalHairColor; 53 | 54 | Name = new GhostName(this, Data?.Name ?? ""); 55 | } 56 | 57 | public override void Added(Scene scene) { 58 | base.Added(scene); 59 | 60 | Hair.Facing = Frame.Data.Facing; 61 | Hair.Start(); 62 | UpdateHair(); 63 | 64 | Scene.Add(Name); 65 | } 66 | 67 | public override void Removed(Scene scene) { 68 | base.Removed(scene); 69 | 70 | Name.RemoveSelf(); 71 | } 72 | 73 | public void UpdateHair() { 74 | if (!Frame.Data.IsValid) 75 | return; 76 | 77 | Hair.Color = new Color( 78 | (Frame.Data.HairColor.R * Color.R) / 255, 79 | (Frame.Data.HairColor.G * Color.G) / 255, 80 | (Frame.Data.HairColor.B * Color.B) / 255, 81 | (Frame.Data.HairColor.A * Color.A) / 255 82 | ); 83 | Hair.Alpha = alphaHair; 84 | Hair.Facing = Frame.Data.Facing; 85 | Hair.SimulateMotion = Frame.Data.HairSimulateMotion; 86 | } 87 | 88 | public void UpdateSprite() { 89 | if (!Frame.Data.IsValid) 90 | return; 91 | 92 | Position = Frame.Data.Position; 93 | Sprite.Rotation = Frame.Data.Rotation; 94 | Sprite.Scale = Frame.Data.Scale; 95 | Sprite.Scale.X = Sprite.Scale.X * (float) Frame.Data.Facing; 96 | Sprite.Color = new Color( 97 | (Frame.Data.Color.R * Color.R) / 255, 98 | (Frame.Data.Color.G * Color.G) / 255, 99 | (Frame.Data.Color.B * Color.B) / 255, 100 | (Frame.Data.Color.A * Color.A) / 255 101 | ) * alpha; 102 | 103 | Sprite.Rate = Frame.Data.SpriteRate; 104 | Sprite.Justify = Frame.Data.SpriteJustify; 105 | 106 | try { 107 | if (Sprite.CurrentAnimationID != Frame.Data.CurrentAnimationID) 108 | Sprite.Play(Frame.Data.CurrentAnimationID); 109 | Sprite.SetAnimationFrame(Frame.Data.CurrentAnimationFrame); 110 | } catch { 111 | // Play likes to fail randomly as the ID doesn't exist in an underlying dict. 112 | // Let's ignore this for now. 113 | } 114 | } 115 | 116 | public override void Update() { 117 | Visible = ForcedFrame != null || ((GhostModule.Settings.Mode & GhostModuleMode.Play) == GhostModuleMode.Play); 118 | Visible &= Frame.Data.IsValid; 119 | if (ForcedFrame == null && Data != null && Data.Dead) 120 | Visible &= GhostModule.Settings.ShowDeaths; 121 | if (ForcedFrame == null && Data != null && !string.IsNullOrEmpty(GhostModule.Settings.NameFilter)) 122 | Visible &= string.IsNullOrEmpty(Data.Name) || GhostModule.Settings.NameFilter.Equals(Data.Name, StringComparison.InvariantCultureIgnoreCase); 123 | 124 | if (ForcedFrame == null && Data != null && Player.InControl && AutoForward) { 125 | do { 126 | FrameIndex++; 127 | } while ( 128 | (PrevFrame.Data.IsValid && !PrevFrame.Data.InControl) || // Skip any frames we're not in control in. 129 | (!PrevFrame.Data.IsValid && FrameIndex < Data.Frames.Count) // Skip any frames not containing the data chunk. 130 | ); 131 | } 132 | 133 | if (Data != null && Data.Opacity != null) { 134 | alpha = Data.Opacity.Value; 135 | alphaHair = Data.Opacity.Value; 136 | } else { 137 | float dist = (Player.Position - Position).LengthSquared(); 138 | dist -= GhostModule.Settings.InnerRadiusDist; 139 | if (dist < 0f) 140 | dist = 0f; 141 | if (GhostModule.Settings.BorderSize == 0) { 142 | dist = dist < GhostModule.Settings.InnerRadiusDist ? 0f : 1f; 143 | } else { 144 | dist /= GhostModule.Settings.BorderSizeDist; 145 | } 146 | alpha = Calc.LerpClamp(GhostModule.Settings.InnerOpacityFactor, GhostModule.Settings.OuterOpacityFactor, dist); 147 | alphaHair = Calc.LerpClamp(GhostModule.Settings.InnerHairOpacityFactor, GhostModule.Settings.OuterHairOpacityFactor, dist); 148 | } 149 | 150 | UpdateSprite(); 151 | UpdateHair(); 152 | 153 | Visible &= alpha > 0f; 154 | 155 | Name.Alpha = Visible ? alpha : 0f; 156 | 157 | base.Update(); 158 | } 159 | 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /GhostMod/GhostChunkData.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost { 14 | public struct GhostChunkData { 15 | 16 | public const string ChunkV1 = "data"; 17 | public const string ChunkV2 = "data2"; 18 | public const string Chunk = ChunkV2; 19 | public bool IsValid; 20 | 21 | // V1 22 | 23 | public bool InControl; 24 | 25 | public Vector2 Position; 26 | public Vector2 Speed; 27 | public float Rotation; 28 | public Vector2 Scale; 29 | public Color Color; 30 | 31 | public float SpriteRate; 32 | public Vector2? SpriteJustify; 33 | 34 | public Facings Facing; 35 | 36 | public string CurrentAnimationID; 37 | public int CurrentAnimationFrame; 38 | 39 | public Color HairColor; 40 | public bool HairSimulateMotion; 41 | 42 | // V2 43 | 44 | public Color? DashColor; 45 | public Vector2 DashDir; 46 | public bool DashWasB; 47 | 48 | public void Read(BinaryReader reader, int version) { 49 | IsValid = true; 50 | 51 | InControl = reader.ReadBoolean(); 52 | 53 | Position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 54 | Speed = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 55 | Rotation = reader.ReadSingle(); 56 | Scale = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 57 | Color = new Color(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); 58 | 59 | SpriteRate = reader.ReadSingle(); 60 | SpriteJustify = reader.ReadBoolean() ? (Vector2?) new Vector2(reader.ReadSingle(), reader.ReadSingle()) : null; 61 | 62 | Facing = (Facings) reader.ReadInt32(); 63 | 64 | CurrentAnimationID = reader.ReadNullTerminatedString(); 65 | CurrentAnimationFrame = reader.ReadInt32(); 66 | 67 | HairColor = new Color(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); 68 | HairSimulateMotion = reader.ReadBoolean(); 69 | 70 | if (version < 2) 71 | return; 72 | 73 | DashColor = reader.ReadBoolean() ? (Color?) new Color(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()) : null; 74 | DashDir = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 75 | DashWasB = reader.ReadBoolean(); 76 | } 77 | 78 | public void Write(BinaryWriter writer) { 79 | writer.Write(InControl); 80 | 81 | writer.Write(Position.X); 82 | writer.Write(Position.Y); 83 | 84 | writer.Write(Speed.X); 85 | writer.Write(Speed.Y); 86 | 87 | writer.Write(Rotation); 88 | 89 | writer.Write(Scale.X); 90 | writer.Write(Scale.Y); 91 | 92 | writer.Write(Color.R); 93 | writer.Write(Color.G); 94 | writer.Write(Color.B); 95 | writer.Write(Color.A); 96 | 97 | writer.Write(SpriteRate); 98 | 99 | if (SpriteJustify != null) { 100 | writer.Write(true); 101 | writer.Write(SpriteJustify.Value.X); 102 | writer.Write(SpriteJustify.Value.Y); 103 | } else { 104 | writer.Write(false); 105 | } 106 | 107 | writer.Write((int) Facing); 108 | 109 | writer.WriteNullTerminatedString(CurrentAnimationID); 110 | writer.Write(CurrentAnimationFrame); 111 | 112 | writer.Write(HairColor.R); 113 | writer.Write(HairColor.G); 114 | writer.Write(HairColor.B); 115 | writer.Write(HairColor.A); 116 | 117 | writer.Write(HairSimulateMotion); 118 | 119 | if (DashColor == null) { 120 | writer.Write(false); 121 | } else { 122 | writer.Write(true); 123 | writer.Write(DashColor.Value.R); 124 | writer.Write(DashColor.Value.G); 125 | writer.Write(DashColor.Value.B); 126 | writer.Write(DashColor.Value.A); 127 | } 128 | 129 | writer.Write(DashDir.X); 130 | writer.Write(DashDir.Y); 131 | 132 | writer.Write(DashWasB); 133 | } 134 | 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /GhostMod/GhostChunkInput.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost { 14 | public struct GhostChunkInput { 15 | 16 | public const string Chunk = "input"; 17 | public bool IsValid; 18 | 19 | public int MoveX; 20 | public int MoveY; 21 | 22 | public Vector2 Aim; 23 | public Vector2 MountainAim; 24 | 25 | public int Buttons; 26 | public bool ESC { 27 | get { 28 | return (Buttons & (int) ButtonMask.ESC) == (int) ButtonMask.ESC; 29 | } 30 | set { 31 | Buttons &= (int) ~ButtonMask.ESC; 32 | if (value) 33 | Buttons |= (int) ButtonMask.ESC; 34 | } 35 | } 36 | public bool Pause { 37 | get { 38 | return (Buttons & (int) ButtonMask.Pause) == (int) ButtonMask.Pause; 39 | } 40 | set { 41 | Buttons &= (int) ~ButtonMask.Pause; 42 | if (value) 43 | Buttons |= (int) ButtonMask.Pause; 44 | } 45 | } 46 | public bool MenuLeft { 47 | get { 48 | return (Buttons & (int) ButtonMask.MenuLeft) == (int) ButtonMask.MenuLeft; 49 | } 50 | set { 51 | Buttons &= (int) ~ButtonMask.MenuLeft; 52 | if (value) 53 | Buttons |= (int) ButtonMask.MenuLeft; 54 | } 55 | } 56 | public bool MenuRight { 57 | get { 58 | return (Buttons & (int) ButtonMask.MenuRight) == (int) ButtonMask.MenuRight; 59 | } 60 | set { 61 | Buttons &= (int) ~ButtonMask.MenuRight; 62 | if (value) 63 | Buttons |= (int) ButtonMask.MenuRight; 64 | } 65 | } 66 | public bool MenuUp { 67 | get { 68 | return (Buttons & (int) ButtonMask.MenuUp) == (int) ButtonMask.MenuUp; 69 | } 70 | set { 71 | Buttons &= (int) ~ButtonMask.MenuUp; 72 | if (value) 73 | Buttons |= (int) ButtonMask.MenuUp; 74 | } 75 | } 76 | public bool MenuDown { 77 | get { 78 | return (Buttons & (int) ButtonMask.MenuDown) == (int) ButtonMask.MenuDown; 79 | } 80 | set { 81 | Buttons &= (int) ~ButtonMask.MenuDown; 82 | if (value) 83 | Buttons |= (int) ButtonMask.MenuDown; 84 | } 85 | } 86 | public bool MenuConfirm { 87 | get { 88 | return (Buttons & (int) ButtonMask.MenuConfirm) == (int) ButtonMask.MenuConfirm; 89 | } 90 | set { 91 | Buttons &= (int) ~ButtonMask.MenuConfirm; 92 | if (value) 93 | Buttons |= (int) ButtonMask.MenuConfirm; 94 | } 95 | } 96 | public bool MenuCancel { 97 | get { 98 | return (Buttons & (int) ButtonMask.MenuCancel) == (int) ButtonMask.MenuCancel; 99 | } 100 | set { 101 | Buttons &= (int) ~ButtonMask.MenuCancel; 102 | if (value) 103 | Buttons |= (int) ButtonMask.MenuCancel; 104 | } 105 | } 106 | public bool MenuJournal { 107 | get { 108 | return (Buttons & (int) ButtonMask.MenuJournal) == (int) ButtonMask.MenuJournal; 109 | } 110 | set { 111 | Buttons &= (int) ~ButtonMask.MenuJournal; 112 | if (value) 113 | Buttons |= (int) ButtonMask.MenuJournal; 114 | } 115 | } 116 | public bool QuickRestart { 117 | get { 118 | return (Buttons & (int) ButtonMask.QuickRestart) == (int) ButtonMask.QuickRestart; 119 | } 120 | set { 121 | Buttons &= (int) ~ButtonMask.QuickRestart; 122 | if (value) 123 | Buttons |= (int) ButtonMask.QuickRestart; 124 | } 125 | } 126 | public bool Jump { 127 | get { 128 | return (Buttons & (int) ButtonMask.Jump) == (int) ButtonMask.Jump; 129 | } 130 | set { 131 | Buttons &= (int) ~ButtonMask.Jump; 132 | if (value) 133 | Buttons |= (int) ButtonMask.Jump; 134 | } 135 | } 136 | public bool Dash { 137 | get { 138 | return (Buttons & (int) ButtonMask.Dash) == (int) ButtonMask.Dash; 139 | } 140 | set { 141 | Buttons &= (int) ~ButtonMask.Dash; 142 | if (value) 143 | Buttons |= (int) ButtonMask.Dash; 144 | } 145 | } 146 | public bool Grab { 147 | get { 148 | return (Buttons & (int) ButtonMask.Grab) == (int) ButtonMask.Grab; 149 | } 150 | set { 151 | Buttons &= (int) ~ButtonMask.Grab; 152 | if (value) 153 | Buttons |= (int) ButtonMask.Grab; 154 | } 155 | } 156 | public bool Talk { 157 | get { 158 | return (Buttons & (int) ButtonMask.Talk) == (int) ButtonMask.Talk; 159 | } 160 | set { 161 | Buttons &= (int) ~ButtonMask.Talk; 162 | if (value) 163 | Buttons |= (int) ButtonMask.Talk; 164 | } 165 | } 166 | 167 | public void Read(BinaryReader reader) { 168 | IsValid = true; 169 | 170 | MoveX = reader.ReadInt32(); 171 | MoveY = reader.ReadInt32(); 172 | 173 | Aim = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 174 | MountainAim = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 175 | 176 | Buttons = reader.ReadInt32(); 177 | } 178 | 179 | public void Write(BinaryWriter writer) { 180 | writer.Write(MoveX); 181 | writer.Write(MoveY); 182 | 183 | writer.Write(Aim.X); 184 | writer.Write(Aim.Y); 185 | 186 | writer.Write(MountainAim.X); 187 | writer.Write(MountainAim.Y); 188 | 189 | writer.Write(Buttons); 190 | } 191 | 192 | [Flags] 193 | public enum ButtonMask : int { 194 | ESC = 1 << 0, 195 | Pause = 1 << 1, 196 | MenuLeft = 1 << 2, 197 | MenuRight = 1 << 3, 198 | MenuUp = 1 << 4, 199 | MenuDown = 1 << 5, 200 | MenuConfirm = 1 << 6, 201 | MenuCancel = 1 << 7, 202 | MenuJournal = 1 << 8, 203 | QuickRestart = 1 << 9, 204 | Jump = 1 << 10, 205 | Dash = 1 << 11, 206 | Grab = 1 << 12, 207 | Talk = 1 << 13 208 | } 209 | 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /GhostMod/GhostData.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | using System.IO; 12 | using System.Text.RegularExpressions; 13 | using System.Globalization; 14 | 15 | namespace Celeste.Mod.Ghost { 16 | public class GhostData { 17 | 18 | public readonly static string Magic = "everest-ghost\r\n"; 19 | public readonly static char[] MagicChars = Magic.ToCharArray(); 20 | 21 | public readonly static int Version = 1; 22 | 23 | public readonly static Regex PathVerifyRegex = new Regex("[\"`?* #" + Regex.Escape(new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars())) + "]", RegexOptions.Compiled); 24 | 25 | public static string GetGhostFilePrefix(Session session) 26 | => GetGhostFilePrefix(session.Area.GetSID(), session.Area.Mode, session.Level); 27 | public static string GetGhostFilePrefix(string sid, AreaMode mode, string level) 28 | => PathVerifyRegex.Replace($"{sid}-{(char) ('A' + (int) mode)}-{level}-", "-"); 29 | 30 | public static string GetGhostFilePath(Session session, string name, DateTime date) 31 | => GetGhostFilePath(session.Area.GetSID(), session.Area.Mode, session.Level, name, date); 32 | public static string GetGhostFilePath(string sid, AreaMode mode, string level, string name, DateTime date) 33 | => Path.Combine( 34 | GhostModule.PathGhosts, 35 | GetGhostFilePrefix(sid, mode, level) + PathVerifyRegex.Replace($"{name}-{date.ToString("yyyy-MM-dd-HH-mm-ss-fff", CultureInfo.InvariantCulture)}", "-") + ".oshiro" 36 | ); 37 | 38 | public static string[] GetAllGhostFilePaths(Session session) 39 | => Directory.GetFiles( 40 | GhostModule.PathGhosts, 41 | GetGhostFilePrefix(session) + "*.oshiro" 42 | ); 43 | public static List ReadAllGhosts(Session session, List list = null) { 44 | if (list == null) 45 | list = new List(); 46 | 47 | foreach (string filePath in GetAllGhostFilePaths(session)) { 48 | GhostData ghost = new GhostData(filePath).Read(); 49 | if (ghost == null) 50 | continue; 51 | list.Add(ghost); 52 | } 53 | 54 | return list; 55 | } 56 | public static void ForAllGhosts(Session session, Func cb) { 57 | if (cb == null) 58 | return; 59 | string[] filePaths = GetAllGhostFilePaths(session); 60 | for (int i = 0; i < filePaths.Length; i++) { 61 | GhostData ghost = new GhostData(filePaths[i]).Read(); 62 | if (ghost == null) 63 | continue; 64 | if (!cb(i, ghost)) 65 | break; 66 | } 67 | } 68 | 69 | public string SID; 70 | public AreaMode Mode; 71 | public string From; 72 | public string Level; 73 | public string Target; 74 | 75 | public string Name; 76 | public DateTime Date; 77 | 78 | public bool Dead; 79 | 80 | public float? Opacity; 81 | 82 | public Guid Run; 83 | 84 | protected string _FilePath; 85 | public string FilePath { 86 | get { 87 | if (_FilePath != null) 88 | return _FilePath; 89 | 90 | return GetGhostFilePath(SID, Mode, Level, Name, Date); 91 | } 92 | set { 93 | _FilePath = value; 94 | } 95 | } 96 | 97 | public List Frames = new List(); 98 | 99 | public GhostFrame this[int i] { 100 | get { 101 | if (i < 0 || i >= Frames.Count) 102 | return default(GhostFrame); 103 | return Frames[i]; 104 | } 105 | } 106 | 107 | public GhostData() { 108 | Date = DateTime.UtcNow; 109 | Run = Guid.NewGuid(); 110 | } 111 | public GhostData(Session session) 112 | : this() { 113 | if (session != null) { 114 | SID = session.Area.GetSID(); 115 | Mode = session.Area.Mode; 116 | Level = session.Level; 117 | } 118 | } 119 | public GhostData(string filePath) 120 | : this() { 121 | FilePath = filePath; 122 | } 123 | 124 | public GhostData Read() { 125 | if (FilePath == null) 126 | // Keep existing frames in-tact. 127 | return null; 128 | 129 | if (!File.Exists(FilePath)) { 130 | // File doesn't exist - load nothing. 131 | Logger.Log("ghost", $"Ghost doesn't exist: {FilePath}"); 132 | Frames = new List(); 133 | return null; 134 | } 135 | 136 | using (Stream stream = File.OpenRead(FilePath)) 137 | using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8)) 138 | return Read(reader); 139 | } 140 | public GhostData Read(BinaryReader reader) { 141 | if (reader.ReadInt16() != 0x0ade) 142 | return null; // Endianness mismatch. 143 | 144 | char[] magic = reader.ReadChars(MagicChars.Length); 145 | if (magic.Length != MagicChars.Length) 146 | return null; // Didn't read as much as we wanted to read. 147 | for (int i = 0; i < MagicChars.Length; i++) { 148 | if (magic[i] != MagicChars[i]) 149 | return null; // Magic mismatch. 150 | } 151 | 152 | int version = reader.ReadInt32(); 153 | // Don't read data from the future, but try to read data from the past. 154 | if (version > Version) 155 | return null; 156 | 157 | int compression = reader.ReadInt32(); 158 | 159 | if (compression != 0) 160 | return null; // Compression not supported yet. 161 | 162 | SID = reader.ReadNullTerminatedString(); 163 | Mode = (AreaMode) reader.ReadInt32(); 164 | Level = reader.ReadNullTerminatedString(); 165 | Target = reader.ReadNullTerminatedString(); 166 | 167 | Name = reader.ReadNullTerminatedString(); 168 | long dateBin = reader.ReadInt64(); 169 | try { 170 | Date = DateTime.FromBinary(dateBin); 171 | } catch { 172 | // The date was invalid. Let's ignore it. 173 | Date = DateTime.UtcNow; 174 | } 175 | 176 | Dead = reader.ReadBoolean(); 177 | 178 | Opacity = reader.ReadBoolean() ? (float?) reader.ReadSingle() : null; 179 | 180 | if (version >= 1) { 181 | Run = new Guid(reader.ReadBytes(16)); 182 | } else { 183 | Run = Guid.Empty; 184 | } 185 | 186 | int count = reader.ReadInt32(); 187 | reader.ReadChar(); // \r 188 | reader.ReadChar(); // \n 189 | Frames = new List(count); 190 | for (int i = 0; i < count; i++) { 191 | GhostFrame frame = new GhostFrame(); 192 | frame.Read(reader); 193 | Frames.Add(frame); 194 | } 195 | 196 | return this; 197 | } 198 | 199 | public void Write() { 200 | if (FilePath == null) 201 | return; 202 | if (FilePath != null && File.Exists(FilePath)) { 203 | // Force ourselves onto the set filepath. 204 | File.Delete(FilePath); 205 | } 206 | 207 | if (!Directory.Exists(Path.GetDirectoryName(FilePath))) 208 | Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); 209 | 210 | using (Stream stream = File.OpenWrite(FilePath)) 211 | using (BinaryWriter writer = new BinaryWriter(stream, Encoding.UTF8)) 212 | Write(writer); 213 | } 214 | public void Write(BinaryWriter writer) { 215 | writer.Write((short) 0x0ade); 216 | writer.Write(MagicChars); 217 | writer.Write(Version); 218 | 219 | writer.Write(0); // Uncompressed 220 | 221 | writer.WriteNullTerminatedString(SID); 222 | writer.Write((int) Mode); 223 | writer.WriteNullTerminatedString(Level); 224 | writer.WriteNullTerminatedString(Target); 225 | 226 | writer.WriteNullTerminatedString(Name); 227 | writer.Write(Date.ToBinary()); 228 | 229 | writer.Write(Dead); 230 | 231 | if (Opacity != null) { 232 | writer.Write(true); 233 | writer.Write(Opacity.Value); 234 | } else { 235 | writer.Write(false); 236 | } 237 | 238 | writer.Write(Run.ToByteArray()); 239 | 240 | writer.Write(Frames.Count); 241 | writer.Write('\r'); 242 | writer.Write('\n'); 243 | for (int i = 0; i < Frames.Count; i++) { 244 | GhostFrame frame = Frames[i]; 245 | frame.Write(writer); 246 | } 247 | } 248 | 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /GhostMod/GhostExtensions.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace Celeste.Mod.Ghost { 14 | public static class GhostExtensions { 15 | 16 | private readonly static FieldInfo f_Player_wasDashB = typeof(Player).GetField("wasDashB", BindingFlags.NonPublic | BindingFlags.Instance); 17 | 18 | public static bool GetWasDashB(this Player self) 19 | => (bool) f_Player_wasDashB.GetValue(self); 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GhostMod/GhostFrame.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost { 14 | public struct GhostFrame { 15 | 16 | public const string End = "\r\n"; 17 | 18 | public GhostChunkData Data; 19 | public GhostChunkInput Input; 20 | 21 | public void Read(BinaryReader reader) { 22 | string chunk; 23 | // The last "chunk" type, \r\n (Windows linebreak), doesn't contain a length. 24 | while ((chunk = reader.ReadNullTerminatedString()) != End) { 25 | uint length = reader.ReadUInt32(); 26 | switch (chunk) { 27 | case GhostChunkData.ChunkV1: 28 | Data.Read(reader, 1); 29 | break; 30 | case GhostChunkData.ChunkV2: 31 | Data.Read(reader, 2); 32 | break; 33 | case GhostChunkInput.Chunk: 34 | Input.Read(reader); 35 | break; 36 | 37 | default: 38 | // Skip any unknown chunks. 39 | reader.BaseStream.Seek(length, SeekOrigin.Current); 40 | break; 41 | } 42 | } 43 | } 44 | 45 | public void Write(BinaryWriter writer) { 46 | if (Data.IsValid) 47 | WriteChunk(writer, Data.Write, GhostChunkData.Chunk); 48 | 49 | if (Input.IsValid) 50 | WriteChunk(writer, Input.Write, GhostChunkInput.Chunk); 51 | 52 | writer.WriteNullTerminatedString(End); 53 | } 54 | 55 | public static void WriteChunk(BinaryWriter writer, Action method, string name) { 56 | long start = WriteChunkStart(writer, name); 57 | method(writer); 58 | WriteChunkEnd(writer, start); 59 | } 60 | 61 | public static long WriteChunkStart(BinaryWriter writer, string name) { 62 | writer.WriteNullTerminatedString(name); 63 | writer.Write(0U); // Filled in later. 64 | long start = writer.BaseStream.Position; 65 | return start; 66 | } 67 | 68 | public static void WriteChunkEnd(BinaryWriter writer, long start) { 69 | long pos = writer.BaseStream.Position; 70 | long length = pos - start; 71 | 72 | // Update the chunk length, which consists of the 4 bytes before the chunk data. 73 | writer.Flush(); 74 | writer.BaseStream.Seek(start - 4, SeekOrigin.Begin); 75 | writer.Write((int) length); 76 | 77 | writer.Flush(); 78 | writer.BaseStream.Seek(pos, SeekOrigin.Begin); 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /GhostMod/GhostInputNodes.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | public static class GhostInputNodes { 14 | 15 | public class MoveX : VirtualAxis.Node { 16 | public GhostInputReplayer Replayer; 17 | public MoveX(GhostInputReplayer replayer) { 18 | Replayer = replayer; 19 | } 20 | public override float Value => Replayer.Frame.Input.MoveX; 21 | } 22 | 23 | public class MoveY : VirtualAxis.Node { 24 | public GhostInputReplayer Replayer; 25 | public MoveY(GhostInputReplayer replayer) { 26 | Replayer = replayer; 27 | } 28 | public override float Value => Replayer.Frame.Input.MoveY; 29 | } 30 | 31 | public class Aim : VirtualJoystick.Node { 32 | public GhostInputReplayer Replayer; 33 | public Aim(GhostInputReplayer replayer) { 34 | Replayer = replayer; 35 | } 36 | public override Vector2 Value => Replayer.Frame.Input.Aim; 37 | } 38 | 39 | public class MountainAim : VirtualJoystick.Node { 40 | public GhostInputReplayer Replayer; 41 | public MountainAim(GhostInputReplayer replayer) { 42 | Replayer = replayer; 43 | } 44 | public override Vector2 Value => Replayer.Frame.Input.MountainAim; 45 | } 46 | 47 | public class Button : VirtualButton.Node { 48 | public GhostInputReplayer Replayer; 49 | public int Mask; 50 | public Button(GhostInputReplayer replayer, GhostChunkInput.ButtonMask mask) { 51 | Replayer = replayer; 52 | Mask = (int) mask; 53 | } 54 | public override bool Check => !MInput.Disabled && (Replayer.Frame.Input.Buttons & Mask) == Mask; 55 | public override bool Pressed => !MInput.Disabled && (Replayer.Frame.Input.Buttons & Mask) == Mask && (Replayer.PrevFrame.Input.Buttons & Mask) == 0; 56 | public override bool Released => !MInput.Disabled && (Replayer.Frame.Input.Buttons & Mask) == 0 && (Replayer.PrevFrame.Input.Buttons & Mask) == Mask; 57 | } 58 | 59 | } 60 | } -------------------------------------------------------------------------------- /GhostMod/GhostInputReplayer.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | // We need this to work across scenes. 14 | public class GhostInputReplayer : GameComponent { 15 | 16 | public GhostData Data; 17 | public int FrameIndex = 0; 18 | public GhostFrame Frame => Data == null ? default(GhostFrame) : Data[FrameIndex]; 19 | public GhostFrame PrevFrame => Data == null ? default(GhostFrame) : Data[FrameIndex - 1]; 20 | 21 | public GhostInputReplayer(Game game, GhostData data) 22 | : base(game) { 23 | Data = data; 24 | 25 | On.Celeste.Input.Initialize += HookInput; 26 | HookInput(null); 27 | } 28 | 29 | public void HookInput(On.Celeste.Input.orig_Initialize orig) { 30 | orig?.Invoke(); 31 | 32 | Input.MoveX.Nodes.Add(new GhostInputNodes.MoveX(this)); 33 | Input.MoveY.Nodes.Add(new GhostInputNodes.MoveY(this)); 34 | 35 | Input.Aim.Nodes.Add(new GhostInputNodes.Aim(this)); 36 | Input.MountainAim.Nodes.Add(new GhostInputNodes.MountainAim(this)); 37 | 38 | Input.ESC.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.ESC)); 39 | Input.Pause.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.Pause)); 40 | Input.MenuLeft.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuLeft)); 41 | Input.MenuRight.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuRight)); 42 | Input.MenuUp.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuUp)); 43 | Input.MenuDown.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuDown)); 44 | Input.MenuConfirm.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuConfirm)); 45 | Input.MenuCancel.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuCancel)); 46 | Input.MenuJournal.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.MenuJournal)); 47 | Input.QuickRestart.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.QuickRestart)); 48 | Input.Jump.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.Jump)); 49 | Input.Dash.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.Dash)); 50 | Input.Grab.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.Grab)); 51 | Input.Talk.Nodes.Add(new GhostInputNodes.Button(this, GhostChunkInput.ButtonMask.Talk)); 52 | 53 | Logger.Log("ghost", "GhostReplayer hooked input."); 54 | } 55 | 56 | public override void Update(GameTime gameTime) { 57 | base.Update(gameTime); 58 | 59 | do { 60 | FrameIndex++; 61 | } while ( 62 | (!Frame.Input.IsValid && FrameIndex < Data.Frames.Count) // Skip any frames not containing the input chunk. 63 | ); 64 | 65 | if (Data == null || FrameIndex >= Data.Frames.Count) 66 | Remove(); 67 | } 68 | 69 | public void Remove() { 70 | On.Celeste.Input.Initialize -= HookInput; 71 | Input.Initialize(); 72 | Logger.Log("ghost", "GhostReplayer returned input."); 73 | Game.Components.Remove(this); 74 | } 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /GhostMod/GhostManager.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | public class GhostManager : Entity { 14 | 15 | public List Ghosts = new List(); 16 | 17 | public Player Player; 18 | 19 | public readonly static Color ColorGold = new Color(1f, 1f, 0f, 1f); 20 | public readonly static Color ColorNeutral = new Color(1f, 1f, 1f, 1f); 21 | 22 | public GhostManager(Player player, Level level) 23 | : base(Vector2.Zero) { 24 | Player = player; 25 | 26 | Tag = Tags.HUD; 27 | 28 | // Read and add all ghosts. 29 | GhostData.ForAllGhosts(level.Session, (i, ghostData) => { 30 | Ghost ghost = new Ghost(player, ghostData); 31 | level.Add(ghost); 32 | Ghosts.Add(ghost); 33 | return true; 34 | }); 35 | } 36 | 37 | public override void Removed(Scene scene) { 38 | base.Removed(scene); 39 | 40 | // Remove any dead ghosts (heh) 41 | for (int i = Ghosts.Count - 1; i > -1; --i) { 42 | Ghost ghost = Ghosts[i]; 43 | if (ghost.Player != Player) 44 | ghost.RemoveSelf(); 45 | } 46 | Ghosts.Clear(); 47 | } 48 | 49 | public override void Render() { 50 | /* Proposed colors: 51 | * blue - full run PB (impossible) 52 | * silver - chapter PB (feasible) 53 | * gold - room PB (done) 54 | */ 55 | 56 | // Gold is the easiest: Find fastest active ghost. 57 | Ghost fastest = null; 58 | foreach (Ghost ghost in Ghosts) { 59 | // While we're at it, reset all colors. 60 | ghost.Color = ColorNeutral; 61 | 62 | if (!ghost.Frame.Data.IsValid) 63 | continue; 64 | 65 | if (fastest == null || ghost.Data.Frames.Count < fastest.Data.Frames.Count) { 66 | fastest = ghost; 67 | } 68 | } 69 | 70 | if (fastest != null) { 71 | fastest.Color = ColorGold; 72 | } 73 | 74 | base.Render(); 75 | } 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /GhostMod/GhostMod.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {4ABF8C07-C533-407E-9EC9-534C2916C907} 8 | Library 9 | Properties 10 | Celeste.Mod.Ghost 11 | GhostMod 12 | v4.5.2 13 | 512 14 | Artifact\ 15 | 16 | 17 | 18 | 19 | true 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | true 23 | full 24 | AnyCPU 25 | prompt 26 | 4 27 | 28 | 29 | bin\Release\ 30 | TRACE 31 | true 32 | true 33 | pdbonly 34 | AnyCPU 35 | prompt 36 | 4 37 | 38 | 39 | 40 | ..\Everest\lib-stripped\Celeste.exe 41 | False 42 | 43 | 44 | ..\Everest\lib-stripped\FNA.dll 45 | False 46 | 47 | 48 | ..\deps\MMHOOK_Celeste.dll 49 | False 50 | 51 | 52 | ..\Everest\lib-stripped\Steamworks.NET.dll 53 | False 54 | 55 | 56 | 57 | 58 | ..\Everest\lib\YamlDotNet.dll 59 | False 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Content\Dialog\English.txt 81 | 82 | 83 | 84 | 85 | {d5d0239d-ff95-4897-9484-1898ab7e82f5} 86 | Celeste.Mod.mm 87 | False 88 | False 89 | 90 | 91 | 92 | 93 | everest.yaml 94 | PreserveNewest 95 | 96 | 97 | 98 | 105 | -------------------------------------------------------------------------------- /GhostMod/GhostModule.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using FMOD.Studio; 13 | 14 | namespace Celeste.Mod.Ghost { 15 | public class GhostModule : EverestModule { 16 | 17 | public static GhostModule Instance; 18 | 19 | public override Type SettingsType => typeof(GhostModuleSettings); 20 | public static GhostModuleSettings Settings => (GhostModuleSettings) Instance._Settings; 21 | 22 | public static bool SettingsOverridden = false; 23 | 24 | public static string PathGhosts { get; internal set; } 25 | 26 | public GhostManager GhostManager; 27 | public GhostRecorder GhostRecorder; 28 | 29 | public Guid Run; 30 | 31 | public GhostModule() { 32 | Instance = this; 33 | 34 | } 35 | 36 | public override void Load() { 37 | PathGhosts = Path.Combine(Everest.PathSettings, "Ghosts"); 38 | if (!Directory.Exists(PathGhosts)) 39 | Directory.CreateDirectory(PathGhosts); 40 | 41 | On.Celeste.Level.LoadLevel += OnLoadLevel; 42 | Everest.Events.Level.OnExit += OnExit; 43 | On.Celeste.Player.Die += OnDie; 44 | } 45 | 46 | public override void Unload() { 47 | On.Celeste.Level.LoadLevel -= OnLoadLevel; 48 | Everest.Events.Level.OnExit -= OnExit; 49 | On.Celeste.Player.Die -= OnDie; 50 | } 51 | 52 | public void OnLoadLevel(On.Celeste.Level.orig_LoadLevel orig, Level level, Player.IntroTypes playerIntro, bool isFromLoader) { 53 | orig(level, playerIntro, isFromLoader); 54 | 55 | if (isFromLoader) { 56 | GhostManager?.RemoveSelf(); 57 | GhostManager = null; 58 | GhostRecorder?.RemoveSelf(); 59 | GhostRecorder = null; 60 | Run = Guid.NewGuid(); 61 | } 62 | 63 | Step(level); 64 | } 65 | 66 | public void OnExit(Level level, LevelExit exit, LevelExit.Mode mode, Session session, HiresSnow snow) { 67 | if (mode == LevelExit.Mode.Completed || 68 | mode == LevelExit.Mode.CompletedInterlude) { 69 | Step(level); 70 | } 71 | } 72 | 73 | public void Step(Level level) { 74 | if (Settings.Mode == GhostModuleMode.Off) 75 | return; 76 | 77 | string target = level.Session.Level; 78 | Logger.Log("ghost", $"Stepping into {level.Session.Area.GetSID()} {target}"); 79 | 80 | Player player = level.Tracker.GetEntity(); 81 | 82 | // Write the ghost, even if we haven't gotten an IL PB. 83 | // Maybe we left the level prematurely earlier? 84 | if (GhostRecorder?.Data != null && 85 | (Settings.Mode & GhostModuleMode.Record) == GhostModuleMode.Record) { 86 | GhostRecorder.Data.Target = target; 87 | GhostRecorder.Data.Run = Run; 88 | GhostRecorder.Data.Write(); 89 | } 90 | 91 | GhostManager?.RemoveSelf(); 92 | 93 | level.Add(GhostManager = new GhostManager(player, level)); 94 | 95 | if (GhostRecorder != null) 96 | GhostRecorder.RemoveSelf(); 97 | level.Add(GhostRecorder = new GhostRecorder(player)); 98 | GhostRecorder.Data = new GhostData(level.Session); 99 | GhostRecorder.Data.Name = Settings.Name; 100 | } 101 | 102 | public PlayerDeadBody OnDie(On.Celeste.Player.orig_Die orig, Player player, Vector2 direction, bool evenIfInvincible, bool registerDeathInStats) { 103 | PlayerDeadBody corpse = orig(player, direction, evenIfInvincible, registerDeathInStats); 104 | 105 | if (GhostRecorder == null || GhostRecorder.Data == null) 106 | return corpse; 107 | 108 | // This is hacky, but it works: 109 | // Check the stack trace for Celeste.Level+* * 110 | // and throw away the data when we're just retrying. 111 | foreach (StackFrame frame in new StackTrace().GetFrames()) { 112 | MethodBase method = frame?.GetMethod(); 113 | if (method == null || method.DeclaringType == null) 114 | continue; 115 | if (!method.DeclaringType.FullName.StartsWith("Celeste.Level+") || 116 | !method.Name.StartsWith("")) 117 | continue; 118 | 119 | GhostRecorder.Data = null; 120 | return corpse; 121 | } 122 | 123 | GhostRecorder.Data.Dead = true; 124 | 125 | return corpse; 126 | } 127 | 128 | public override void CreateModMenuSection(TextMenu menu, bool inGame, FMOD.Studio.EventInstance snapshot) { 129 | if (SettingsOverridden && !Settings.AlwaysShowSettings) { 130 | menu.Add(new TextMenu.SubHeader(Dialog.Clean("modoptions_ghostmodule_overridden") + " | v." + Metadata.VersionString)); 131 | return; 132 | } 133 | 134 | base.CreateModMenuSection(menu, inGame, snapshot); 135 | } 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /GhostMod/GhostModuleSettings.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | public class GhostModuleSettings : EverestModuleSettings { 14 | 15 | [SettingIgnore] 16 | public bool AlwaysShowSettings { get; set; } = false; 17 | 18 | public GhostModuleMode Mode { get; set; } = GhostModuleMode.On; 19 | 20 | [SettingInGame(false)] 21 | public string Name { get; set; } = ""; 22 | 23 | [SettingIgnore] // Ignore on older builds of Everest which don't support custom entry creators. 24 | public string NameFilter { get; set; } = ""; 25 | 26 | public bool ShowNames { get; set; } = true; 27 | 28 | public bool ShowDeaths { get; set; } = false; 29 | 30 | [SettingRange(0, 10)] 31 | public int InnerOpacity { get; set; } = 4; 32 | [YamlIgnore] 33 | [SettingIgnore] 34 | public float InnerOpacityFactor => InnerOpacity / 10f; 35 | 36 | [SettingRange(0, 10)] 37 | public int InnerHairOpacity { get; set; } = 4; 38 | [YamlIgnore] 39 | [SettingIgnore] 40 | public float InnerHairOpacityFactor => InnerHairOpacity / 10f; 41 | 42 | [SettingRange(0, 10)] 43 | public int OuterOpacity { get; set; } = 1; 44 | [YamlIgnore] 45 | [SettingIgnore] 46 | public float OuterOpacityFactor => OuterOpacity / 10f; 47 | 48 | [SettingRange(0, 10)] 49 | public int OuterHairOpacity { get; set; } = 1; 50 | [YamlIgnore] 51 | [SettingIgnore] 52 | public float OuterHairOpacityFactor => OuterHairOpacity / 10f; 53 | 54 | [SettingRange(0, 10)] 55 | public int InnerRadius { get; set; } = 4; 56 | [YamlIgnore] 57 | [SettingIgnore] 58 | public float InnerRadiusDist => InnerRadius * InnerRadius * 64f; 59 | 60 | [SettingRange(0, 10)] 61 | public int BorderSize { get; set; } = 4; 62 | [YamlIgnore] 63 | [SettingIgnore] 64 | public float BorderSizeDist => BorderSize * BorderSize * 64f; 65 | 66 | public void ShowNameFilterEntry(TextMenu menu, bool inGame) { 67 | // TODO: Create a slider to choose between all available names. 68 | } 69 | 70 | } 71 | public enum GhostModuleMode { 72 | Off = 0, 73 | Record = 1 << 0, 74 | Play = 1 << 1, 75 | On = Record | Play 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /GhostMod/GhostName.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | public class GhostName : Entity { 14 | 15 | public Entity Tracking; 16 | public string Name; 17 | 18 | protected Camera Camera; 19 | 20 | public float Alpha = 1f; 21 | 22 | public GhostName(Entity tracking, string name) 23 | : base(Vector2.Zero) { 24 | Tracking = tracking; 25 | Name = name; 26 | 27 | Tag = TagsExt.SubHUD; 28 | } 29 | 30 | public override void Render() { 31 | base.Render(); 32 | 33 | if (Alpha <= 0f || string.IsNullOrWhiteSpace(Name)) 34 | return; 35 | 36 | if (!GhostModule.Settings.ShowNames || 37 | Tracking == null) 38 | return; 39 | 40 | Level level = SceneAs(); 41 | if (level == null /*|| level.FrozenOrPaused || level.RetryPlayerCorpse != null || level.SkippingCutscene*/) 42 | return; 43 | 44 | if (Camera == null) 45 | Camera = level.Camera; 46 | if (Camera == null) 47 | return; 48 | 49 | Vector2 pos = Tracking.Position; 50 | pos.Y -= 16f; 51 | 52 | pos -= level.Camera.Position; 53 | pos *= 6f; // 1920 / 320 54 | 55 | Vector2 size = ActiveFont.Measure(Name); 56 | pos = pos.Clamp( 57 | 0f + size.X * 0.5f, 0f + size.Y * 1f, 58 | 1920f - size.X * 0.5f, 1080f 59 | ); 60 | 61 | ActiveFont.DrawOutline( 62 | Name, 63 | pos, 64 | new Vector2(0.5f, 1f), 65 | Vector2.One * 0.5f, 66 | Color.White * Alpha, 67 | 2f, 68 | Color.Black * (Alpha * Alpha * Alpha) 69 | ); 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /GhostMod/GhostRecorder.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost { 13 | public class GhostRecorder : Entity { 14 | 15 | public Player Player; 16 | 17 | public GhostData Data; 18 | 19 | public GhostFrame LastFrameData; 20 | public GhostFrame LastFrameInput; 21 | 22 | public GhostRecorder(Player player) 23 | : base() { 24 | Player = player; 25 | Tag = Tags.HUD; 26 | } 27 | 28 | public override void Update() { 29 | base.Update(); 30 | 31 | RecordData(); 32 | } 33 | 34 | public override void Render() { 35 | base.Render(); 36 | 37 | RecordInput(); 38 | } 39 | 40 | public void RecordData() { 41 | if (Player == null) 42 | return; 43 | 44 | // A data frame is always a new frame, no matter if the previous one lacks data or not. 45 | LastFrameData = new GhostFrame { 46 | Data = new GhostChunkData { 47 | IsValid = true, 48 | 49 | InControl = Player.InControl, 50 | 51 | Position = Player.Position, 52 | Speed = Player.Speed, 53 | Rotation = Player.Sprite.Rotation, 54 | Scale = Player.Sprite.Scale, 55 | Color = Player.Sprite.Color, 56 | 57 | Facing = Player.Facing, 58 | 59 | CurrentAnimationID = Player.Sprite.CurrentAnimationID, 60 | CurrentAnimationFrame = Player.Sprite.CurrentAnimationFrame, 61 | 62 | HairColor = Player.Hair.Color, 63 | HairSimulateMotion = Player.Hair.SimulateMotion, 64 | 65 | DashColor = Player.StateMachine.State == Player.StDash ? Player.GetCurrentTrailColor() : (Color?) null, 66 | DashDir = Player.DashDir, 67 | DashWasB = Player.GetWasDashB() 68 | } 69 | }; 70 | 71 | if (Data != null) 72 | Data.Frames.Add(LastFrameData); 73 | } 74 | 75 | public void RecordInput() { 76 | // Check if we've got a data-less input frame. If so, add input to it. 77 | // If the frame already has got input, add a new input frame. 78 | 79 | bool inputDisabled = MInput.Disabled; 80 | MInput.Disabled = false; 81 | 82 | GhostFrame frame; 83 | bool isNew = false; 84 | if (Data == null || Data.Frames.Count == 0 || Data[Data.Frames.Count - 1].Input.IsValid) { 85 | frame = new GhostFrame(); 86 | isNew = true; 87 | } else { 88 | frame = Data[Data.Frames.Count - 1]; 89 | } 90 | 91 | frame.Input.IsValid = true; 92 | 93 | frame.Input.MoveX = Input.MoveX.Value; 94 | frame.Input.MoveY = Input.MoveY.Value; 95 | 96 | frame.Input.Aim = Input.Aim.Value; 97 | frame.Input.MountainAim = Input.MountainAim.Value; 98 | 99 | frame.Input.ESC = Input.ESC.Check; 100 | frame.Input.Pause = Input.Pause.Check; 101 | frame.Input.MenuLeft = Input.MenuLeft.Check; 102 | frame.Input.MenuRight = Input.MenuRight.Check; 103 | frame.Input.MenuUp = Input.MenuUp.Check; 104 | frame.Input.MenuDown = Input.MenuDown.Check; 105 | frame.Input.MenuConfirm = Input.MenuConfirm.Check; 106 | frame.Input.MenuCancel = Input.MenuCancel.Check; 107 | frame.Input.MenuJournal = Input.MenuJournal.Check; 108 | frame.Input.QuickRestart = Input.QuickRestart.Check; 109 | frame.Input.Jump = Input.Jump.Check; 110 | frame.Input.Dash = Input.Dash.Check; 111 | frame.Input.Grab = Input.Grab.Check; 112 | frame.Input.Talk = Input.Talk.Check; 113 | 114 | if (Data != null) { 115 | if (isNew) { 116 | Data.Frames.Add(frame); 117 | } else { 118 | Data.Frames[Data.Frames.Count - 1] = frame; 119 | } 120 | } 121 | 122 | LastFrameInput = frame; 123 | 124 | MInput.Disabled = inputDisabled; 125 | 126 | } 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /GhostMod/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("GhostMod")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("GhostMod")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("4abf8c07-c533-407e-9ec9-534c2916c907")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkEAudioState.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Embedded AudioState chunk, which is a part of another chunk. 17 | /// 18 | public class ChunkEAudioState : IChunk { 19 | 20 | public const string ChunkID = "nEA"; 21 | 22 | public bool IsValid => Audio != null; 23 | public bool IsSendable => true; 24 | 25 | public AudioState Audio; 26 | 27 | public ChunkEAudioState() { 28 | } 29 | 30 | public ChunkEAudioState(AudioState audio) { 31 | Audio = audio; 32 | } 33 | 34 | public void Read(BinaryReader reader) { 35 | Audio = new AudioState(); 36 | 37 | if (reader.ReadBoolean()) { 38 | ChunkEAudioTrackState track = new ChunkEAudioTrackState(); 39 | track.Read(reader); 40 | Audio.Music = track.Track; 41 | } 42 | 43 | if (reader.ReadBoolean()) { 44 | ChunkEAudioTrackState track = new ChunkEAudioTrackState(); 45 | track.Read(reader); 46 | Audio.Ambience = track.Track; 47 | } 48 | } 49 | 50 | public void Write(BinaryWriter writer) { 51 | if (Audio.Music != null) { 52 | writer.Write(true); 53 | ChunkEAudioTrackState track = new ChunkEAudioTrackState(Audio.Music); 54 | track.Write(writer); 55 | 56 | } else { 57 | writer.Write(false); 58 | } 59 | 60 | if (Audio.Ambience != null) { 61 | writer.Write(true); 62 | ChunkEAudioTrackState track = new ChunkEAudioTrackState(Audio.Ambience); 63 | track.Write(writer); 64 | 65 | } else { 66 | writer.Write(false); 67 | } 68 | } 69 | 70 | public object Clone() 71 | => new ChunkEAudioState { 72 | Audio = Audio.Clone() 73 | }; 74 | 75 | public static implicit operator ChunkEAudioState(GhostNetFrame frame) 76 | => frame.Get(); 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkEAudioTrackState.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using FMOD.Studio; 3 | using Microsoft.Xna.Framework; 4 | using Monocle; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using YamlDotNet.Serialization; 13 | 14 | namespace Celeste.Mod.Ghost.Net { 15 | [Chunk(ChunkID)] 16 | /// 17 | /// Embedded AudioTrackState chunk, which is a part of another chunk. 18 | /// 19 | public class ChunkEAudioTrackState : IChunk { 20 | 21 | public const string ChunkID = "nEAT"; 22 | 23 | public bool IsValid => Track != null; 24 | public bool IsSendable => true; 25 | 26 | public AudioTrackState Track; 27 | 28 | public ChunkEAudioTrackState() { 29 | } 30 | 31 | public ChunkEAudioTrackState(AudioTrackState track) { 32 | Track = track; 33 | } 34 | 35 | public void Read(BinaryReader reader) { 36 | Track = new AudioTrackState(); 37 | 38 | Track.Event = reader.ReadNullTerminatedString(); 39 | Track.Progress = reader.ReadInt32(); 40 | 41 | int count = reader.ReadByte(); 42 | for (int i = 0; i < count; i++) { 43 | MEP param = new MEP(); 44 | param.Key = reader.ReadNullTerminatedString(); 45 | param.Value = reader.ReadSingle(); 46 | Track.Parameters.Add(param); 47 | } 48 | } 49 | 50 | public void Write(BinaryWriter writer) { 51 | writer.WriteNullTerminatedString(Track.Event); 52 | writer.Write(Track.Progress); 53 | 54 | writer.Write((byte) Track.Parameters.Count); 55 | for (int i = 0; i < Track.Parameters.Count; i++) { 56 | MEP param = Track.Parameters[i]; 57 | writer.WriteNullTerminatedString(param.Key); 58 | writer.Write(param.Value); 59 | } 60 | } 61 | 62 | public object Clone() 63 | => new ChunkEAudioTrackState { 64 | Track = Track.Clone() 65 | }; 66 | 67 | public static implicit operator ChunkEAudioTrackState(GhostNetFrame frame) 68 | => frame.Get(); 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkHHead.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Not sent by client, but attached to all frames by client. 17 | /// 18 | public class ChunkHHead : IChunk { 19 | 20 | public const string ChunkID = "nH"; 21 | 22 | public bool IsValid => true; 23 | public bool IsSendable => true; 24 | 25 | public uint PlayerID; 26 | 27 | public void Read(BinaryReader reader) { 28 | PlayerID = reader.ReadUInt32(); 29 | } 30 | 31 | public void Write(BinaryWriter writer) { 32 | writer.Write(PlayerID); 33 | } 34 | 35 | public object Clone() 36 | => new ChunkHHead { 37 | PlayerID = PlayerID 38 | }; 39 | 40 | public static implicit operator ChunkHHead(GhostNetFrame frame) 41 | => frame.Get(); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkMChat.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using FMOD.Studio; 3 | using Microsoft.Xna.Framework; 4 | using Monocle; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using YamlDotNet.Serialization; 13 | 14 | namespace Celeste.Mod.Ghost.Net { 15 | [Chunk(ChunkID)] 16 | /// 17 | /// A simple chat message. 18 | /// 19 | public class ChunkMChat : IChunk { 20 | 21 | public const string ChunkID = "nMC"; 22 | 23 | public bool IsValid => !string.IsNullOrWhiteSpace(Text); 24 | public bool IsSendable => true; 25 | 26 | /// 27 | /// Server-internal field. 28 | /// 29 | public bool CreatedByServer; 30 | /// 31 | /// Server-internal field. 32 | /// 33 | public bool Logged; 34 | 35 | public uint ID; 36 | public string Tag; 37 | public string Text; 38 | public Color Color; 39 | public DateTime Date; 40 | 41 | public void Read(BinaryReader reader) { 42 | ID = reader.ReadUInt32(); 43 | Tag = reader.ReadNullTerminatedString(); 44 | Text = reader.ReadNullTerminatedString(); 45 | Color = new Color(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), 255); 46 | Date = DateTime.FromBinary(reader.ReadInt64()); 47 | } 48 | 49 | public void Write(BinaryWriter writer) { 50 | writer.Write(ID); 51 | writer.WriteNullTerminatedString(Tag); 52 | writer.WriteNullTerminatedString(Text); 53 | writer.Write(Color.R); 54 | writer.Write(Color.G); 55 | writer.Write(Color.B); 56 | writer.Write(Date.ToBinary()); 57 | } 58 | 59 | public object Clone() 60 | => new ChunkMChat { 61 | CreatedByServer = CreatedByServer, 62 | Logged = Logged, 63 | 64 | ID = ID, 65 | Tag = Tag, 66 | Text = Text, 67 | Color = Color, 68 | Date = Date 69 | }; 70 | 71 | public static implicit operator ChunkMChat(GhostNetFrame frame) 72 | => frame.Get(); 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkMEmote.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Make an emote spawn above the player. 17 | /// 18 | public class ChunkMEmote : IChunk { 19 | 20 | public const string ChunkID = "nME"; 21 | 22 | public bool IsValid => !string.IsNullOrWhiteSpace(Value); 23 | public bool IsSendable => true; 24 | 25 | public string Value; 26 | 27 | public void Read(BinaryReader reader) { 28 | Value = reader.ReadNullTerminatedString(); 29 | } 30 | 31 | public void Write(BinaryWriter writer) { 32 | writer.WriteNullTerminatedString(Value); 33 | } 34 | 35 | public object Clone() 36 | => new ChunkMEmote { 37 | Value = Value 38 | }; 39 | 40 | public static implicit operator ChunkMEmote(GhostNetFrame frame) 41 | => frame.Get(); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkMPlayer.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using FMOD.Studio; 3 | using Microsoft.Xna.Framework; 4 | using Monocle; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using YamlDotNet.Serialization; 13 | 14 | namespace Celeste.Mod.Ghost.Net { 15 | [Chunk(ChunkID)] 16 | /// 17 | /// "Status" chunk sent on connection and on room change. 18 | /// Server remembers this and responds with all other players in the same room. 19 | /// If the player receives this with their own player ID, the server is moving the player. 20 | /// 21 | public class ChunkMPlayer : IChunk { 22 | 23 | public const string ChunkID = "nM"; 24 | 25 | public bool IsValid => true; 26 | public bool IsSendable => !IsCached; 27 | 28 | /// 29 | /// Whether the chunk is what the server / client last remembers about the player, or if it's a newly received chunk. 30 | /// 31 | public bool IsCached; 32 | 33 | public bool IsEcho; 34 | 35 | public string Name; 36 | 37 | public string SID; 38 | public AreaMode Mode; 39 | public string Level; 40 | 41 | public bool LevelCompleted = false; 42 | public LevelExit.Mode? LevelExit; 43 | 44 | public bool Idle; 45 | 46 | public void Read(BinaryReader reader) { 47 | IsCached = false; 48 | 49 | IsEcho = reader.ReadBoolean(); 50 | 51 | Name = reader.ReadNullTerminatedString(); 52 | 53 | SID = reader.ReadNullTerminatedString(); 54 | Mode = (AreaMode) reader.ReadByte(); 55 | Level = reader.ReadNullTerminatedString(); 56 | 57 | LevelCompleted = reader.ReadBoolean(); 58 | 59 | if (reader.ReadBoolean()) 60 | LevelExit = (LevelExit.Mode) reader.ReadByte(); 61 | 62 | Idle = reader.ReadBoolean(); 63 | } 64 | 65 | public void Write(BinaryWriter writer) { 66 | writer.Write(IsEcho); 67 | 68 | writer.WriteNullTerminatedString(Name); 69 | 70 | writer.WriteNullTerminatedString(SID); 71 | writer.Write((byte) Mode); 72 | writer.WriteNullTerminatedString(Level); 73 | 74 | writer.Write(LevelCompleted); 75 | 76 | if (LevelExit == null) { 77 | writer.Write(false); 78 | } else { 79 | writer.Write(true); 80 | writer.Write((byte) LevelExit); 81 | } 82 | 83 | writer.Write(Idle); 84 | } 85 | 86 | public object Clone() 87 | => new ChunkMPlayer { 88 | IsEcho = IsEcho, 89 | 90 | Name = Name, 91 | 92 | SID = SID, 93 | Mode = Mode, 94 | Level = Level, 95 | 96 | LevelCompleted = LevelCompleted, 97 | LevelExit = LevelExit, 98 | 99 | Idle = Idle 100 | }; 101 | 102 | public static implicit operator ChunkMPlayer(GhostNetFrame frame) 103 | => frame.Get(); 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkMRequest.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Chunk sent by the server if it's requesting another "requestable" chunk from the client. 17 | /// A client defines what it wants to respond to on its own. The server should be able to deal 18 | /// with the lack of a response natively. 19 | /// 20 | public class ChunkMRequest : IChunk { 21 | 22 | public const string ChunkID = "nMR"; 23 | 24 | public bool IsValid => !string.IsNullOrWhiteSpace(ID); 25 | public bool IsSendable => true; 26 | 27 | public string ID; 28 | 29 | public void Read(BinaryReader reader) { 30 | ID = reader.ReadNullTerminatedString().Trim(); 31 | } 32 | 33 | public void Write(BinaryWriter writer) { 34 | writer.WriteNullTerminatedString(ID); 35 | } 36 | 37 | public object Clone() 38 | => new ChunkMRequest { 39 | ID = ID 40 | }; 41 | 42 | public static implicit operator ChunkMRequest(GhostNetFrame frame) 43 | => frame.Get(); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkMServerInfo.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Sent by the server, chunk containing some basic connection info. 17 | /// 18 | public class ChunkMServerInfo : IChunk { 19 | 20 | public const string ChunkID = "nM?"; 21 | 22 | public bool IsValid => true; 23 | public bool IsSendable => true; 24 | 25 | // PlayerID contained in HHead. 26 | 27 | public string Name; 28 | 29 | public void Read(BinaryReader reader) { 30 | Name = reader.ReadNullTerminatedString(); 31 | } 32 | 33 | public void Write(BinaryWriter writer) { 34 | writer.WriteNullTerminatedString(Name); 35 | } 36 | 37 | public object Clone() 38 | => new ChunkMServerInfo { 39 | Name = Name 40 | }; 41 | 42 | public static implicit operator ChunkMServerInfo(GhostNetFrame frame) 43 | => frame.Get(); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkMSession.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Session "status" chunk that can be requested via ChunkMRequest. 17 | /// Used when teleporting. 18 | /// The player can receive this with an MPlayer chunk to change the session. 19 | /// 20 | public class ChunkMSession : IChunk { 21 | 22 | public const string ChunkID = "nMS"; 23 | 24 | public bool IsValid => true; 25 | public bool IsSendable => true; 26 | 27 | public bool InSession; 28 | 29 | public AudioState Audio; 30 | public Vector2? RespawnPoint; 31 | public PlayerInventory Inventory; 32 | public HashSet Flags; 33 | public HashSet LevelFlags; 34 | public HashSet Strawberries; 35 | public HashSet DoNotLoad; 36 | public HashSet Keys; 37 | public List Counters; 38 | public string FurthestSeenLevel; 39 | public string StartCheckpoint; 40 | public string ColorGrade; 41 | public bool[] SummitGems; 42 | public bool FirstLevel; 43 | public bool Cassette; 44 | public bool HeartGem; 45 | public bool Dreaming; 46 | public bool GrabbedGolden; 47 | public bool HitCheckpoint; 48 | public float LightingAlphaAdd; 49 | public float BloomBaseAdd; 50 | public float DarkRoomAlpha; 51 | public long Time; 52 | public Session.CoreModes CoreMode; 53 | 54 | public void Read(BinaryReader reader) { 55 | InSession = reader.ReadBoolean(); 56 | if (!InSession) 57 | return; 58 | 59 | byte bools; 60 | int count; 61 | 62 | if (reader.ReadBoolean()) { 63 | ChunkEAudioState audio = new ChunkEAudioState(); 64 | audio.Read(reader); 65 | Audio = audio.Audio; 66 | } 67 | 68 | if (reader.ReadBoolean()) 69 | RespawnPoint = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 70 | 71 | Inventory = new PlayerInventory(); 72 | bools = reader.ReadByte(); 73 | Inventory.Backpack = UnpackBool(bools, 0); 74 | Inventory.DreamDash = UnpackBool(bools, 1); 75 | Inventory.NoRefills = UnpackBool(bools, 2); 76 | Inventory.Dashes = reader.ReadByte(); 77 | 78 | Flags = new HashSet(); 79 | count = reader.ReadByte(); 80 | for (int i = 0; i < count; i++) 81 | Flags.Add(reader.ReadNullTerminatedString()); 82 | 83 | LevelFlags = new HashSet(); 84 | count = reader.ReadByte(); 85 | for (int i = 0; i < count; i++) 86 | LevelFlags.Add(reader.ReadNullTerminatedString()); 87 | 88 | Strawberries = new HashSet(); 89 | count = reader.ReadByte(); 90 | for (int i = 0; i < count; i++) 91 | Strawberries.Add(new EntityID(reader.ReadNullTerminatedString(), reader.ReadInt32())); 92 | 93 | DoNotLoad = new HashSet(); 94 | count = reader.ReadByte(); 95 | for (int i = 0; i < count; i++) 96 | DoNotLoad.Add(new EntityID(reader.ReadNullTerminatedString(), reader.ReadInt32())); 97 | 98 | Keys = new HashSet(); 99 | count = reader.ReadByte(); 100 | for (int i = 0; i < count; i++) 101 | Keys.Add(new EntityID(reader.ReadNullTerminatedString(), reader.ReadInt32())); 102 | 103 | Counters = new List(); 104 | count = reader.ReadByte(); 105 | for (int i = 0; i < count; i++) 106 | Counters.Add(new Session.Counter { 107 | Key = reader.ReadNullTerminatedString(), 108 | Value = reader.ReadInt32() 109 | }); 110 | 111 | FurthestSeenLevel = reader.ReadNullTerminatedString()?.Nullify(); 112 | StartCheckpoint = reader.ReadNullTerminatedString()?.Nullify(); 113 | ColorGrade = reader.ReadNullTerminatedString()?.Nullify(); 114 | 115 | count = reader.ReadByte(); 116 | SummitGems = new bool[count]; 117 | for (int i = 0; i < count; i++) { 118 | if ((i % 8) == 0) 119 | bools = reader.ReadByte(); 120 | SummitGems[i] = UnpackBool(bools, i % 8); 121 | } 122 | 123 | bools = reader.ReadByte(); 124 | FirstLevel = UnpackBool(bools, 0); 125 | Cassette = UnpackBool(bools, 1); 126 | HeartGem = UnpackBool(bools, 2); 127 | Dreaming = UnpackBool(bools, 3); 128 | GrabbedGolden = UnpackBool(bools, 4); 129 | HitCheckpoint = UnpackBool(bools, 5); 130 | 131 | LightingAlphaAdd = reader.ReadSingle(); 132 | BloomBaseAdd = reader.ReadSingle(); 133 | DarkRoomAlpha = reader.ReadSingle(); 134 | 135 | Time = reader.ReadInt64(); 136 | 137 | CoreMode = (Session.CoreModes) reader.ReadByte(); 138 | } 139 | 140 | public void Write(BinaryWriter writer) { 141 | if (!InSession) { 142 | writer.Write(false); 143 | return; 144 | } 145 | writer.Write(true); 146 | 147 | byte bools; 148 | 149 | if (Audio != null) { 150 | writer.Write(true); 151 | ChunkEAudioState audio = new ChunkEAudioState(Audio); 152 | audio.Write(writer); 153 | 154 | } else { 155 | writer.Write(false); 156 | } 157 | 158 | if (RespawnPoint != null) { 159 | writer.Write(true); 160 | writer.Write(RespawnPoint.Value.X); 161 | writer.Write(RespawnPoint.Value.Y); 162 | 163 | } else { 164 | writer.Write(false); 165 | } 166 | 167 | writer.Write(PackBools(Inventory.Backpack, Inventory.DreamDash, Inventory.NoRefills)); 168 | writer.Write((byte) Inventory.Dashes); 169 | 170 | writer.Write((byte) Flags.Count); 171 | foreach (string value in Flags) 172 | writer.WriteNullTerminatedString(value); 173 | 174 | writer.Write((byte) LevelFlags.Count); 175 | foreach (string value in LevelFlags) 176 | writer.WriteNullTerminatedString(value); 177 | 178 | writer.Write((byte) Strawberries.Count); 179 | foreach (EntityID value in Strawberries) { 180 | writer.WriteNullTerminatedString(value.Level); 181 | writer.Write(value.ID); 182 | } 183 | 184 | writer.Write((byte) DoNotLoad.Count); 185 | foreach (EntityID value in DoNotLoad) { 186 | writer.WriteNullTerminatedString(value.Level); 187 | writer.Write(value.ID); 188 | } 189 | 190 | writer.Write((byte) Keys.Count); 191 | foreach (EntityID value in Keys) { 192 | writer.WriteNullTerminatedString(value.Level); 193 | writer.Write(value.ID); 194 | } 195 | 196 | writer.Write((byte) Counters.Count); 197 | foreach (Session.Counter value in Counters) { 198 | writer.WriteNullTerminatedString(value.Key); 199 | writer.Write(value.Value); 200 | } 201 | 202 | writer.WriteNullTerminatedString(FurthestSeenLevel); 203 | writer.WriteNullTerminatedString(StartCheckpoint); 204 | writer.WriteNullTerminatedString(ColorGrade); 205 | 206 | writer.Write((byte) SummitGems.Length); 207 | bools = 0; 208 | for (int i = 0; i < SummitGems.Length; i++) { 209 | bools = PackBool(bools, i % 8, SummitGems[i]); 210 | if (((i + 1) % 8) == 0) { 211 | writer.Write(bools); 212 | bools = 0; 213 | } 214 | } 215 | if (SummitGems.Length % 8 != 0) 216 | writer.Write(bools); 217 | 218 | writer.Write(PackBools(FirstLevel, Cassette, HeartGem, Dreaming, GrabbedGolden, HitCheckpoint)); 219 | 220 | writer.Write(LightingAlphaAdd); 221 | writer.Write(BloomBaseAdd); 222 | writer.Write(DarkRoomAlpha); 223 | 224 | writer.Write(Time); 225 | 226 | writer.Write((byte) CoreMode); 227 | } 228 | 229 | public object Clone() 230 | => new ChunkMSession { 231 | InSession = InSession, 232 | 233 | RespawnPoint = RespawnPoint, 234 | Inventory = Inventory, 235 | Flags = Flags, 236 | LevelFlags = LevelFlags, 237 | Strawberries = Strawberries, 238 | DoNotLoad = DoNotLoad, 239 | Keys = Keys, 240 | Counters = Counters, 241 | FurthestSeenLevel = FurthestSeenLevel, 242 | StartCheckpoint = StartCheckpoint, 243 | ColorGrade = ColorGrade, 244 | SummitGems = SummitGems, 245 | FirstLevel = FirstLevel, 246 | Cassette = Cassette, 247 | HeartGem = HeartGem, 248 | Dreaming = Dreaming, 249 | GrabbedGolden = GrabbedGolden, 250 | HitCheckpoint = HitCheckpoint, 251 | LightingAlphaAdd = LightingAlphaAdd, 252 | BloomBaseAdd = BloomBaseAdd, 253 | DarkRoomAlpha = DarkRoomAlpha, 254 | Time = Time, 255 | CoreMode = CoreMode 256 | }; 257 | 258 | public static implicit operator ChunkMSession(GhostNetFrame frame) 259 | => frame.Get(); 260 | 261 | public static byte PackBool(byte value, int index, bool set) { 262 | int mask = 1 << index; 263 | return set ? (byte) (value | mask) : (byte) (value & ~mask); 264 | } 265 | 266 | public static bool UnpackBool(byte value, int index) { 267 | int mask = 1 << index; 268 | return (value & mask) == mask; 269 | } 270 | 271 | public static byte PackBools(bool a = false, bool b = false, bool c = false, bool d = false, bool e = false, bool f = false, bool g = false, bool h = false) { 272 | byte value = 0; 273 | value = PackBool(value, 0, a); 274 | value = PackBool(value, 1, b); 275 | value = PackBool(value, 2, c); 276 | value = PackBool(value, 3, d); 277 | value = PackBool(value, 4, e); 278 | value = PackBool(value, 5, f); 279 | value = PackBool(value, 6, g); 280 | value = PackBool(value, 7, h); 281 | return value; 282 | } 283 | 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkRListAreas.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | public class ChunkRListAreas : IChunk { 16 | 17 | public const string ChunkID = "nRlA"; 18 | 19 | public bool IsValid => true; 20 | public bool IsSendable => true; 21 | 22 | public string[] Entries; 23 | 24 | public void Read(BinaryReader reader) { 25 | Entries = new string[reader.ReadByte()]; 26 | for (int i = 0; i < Entries.Length; i++) { 27 | Entries[i] = reader.ReadNullTerminatedString(); 28 | } 29 | } 30 | 31 | public void Write(BinaryWriter writer) { 32 | if (Entries == null || Entries.Length == 0) { 33 | writer.Write((byte) 0); 34 | return; 35 | } 36 | 37 | writer.Write((byte) Entries.Length); 38 | for (int i = 0; i < Entries.Length; i++) { 39 | writer.WriteNullTerminatedString(Entries[i]); 40 | } 41 | } 42 | 43 | public class Entry { 44 | public string Name; 45 | public Version Version; 46 | } 47 | 48 | public object Clone() 49 | => new ChunkRListAreas { 50 | Entries = Entries 51 | }; 52 | 53 | public static implicit operator ChunkRListAreas(GhostNetFrame frame) 54 | => frame.Get(); 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkRListMods.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | public class ChunkRListMods : IChunk { 16 | 17 | public const string ChunkID = "nRlM"; 18 | 19 | public bool IsValid => true; 20 | public bool IsSendable => true; 21 | 22 | public Entry[] Entries; 23 | 24 | public void Read(BinaryReader reader) { 25 | Entries = new Entry[reader.ReadByte()]; 26 | for (int i = 0; i < Entries.Length; i++) { 27 | Entries[i] = new Entry { 28 | Name = reader.ReadNullTerminatedString(), 29 | Version = new Version(reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32()) 30 | }; 31 | } 32 | } 33 | 34 | public void Write(BinaryWriter writer) { 35 | if (Entries == null || Entries.Length == 0) { 36 | writer.Write((byte) 0); 37 | return; 38 | } 39 | 40 | writer.Write((byte) Entries.Length); 41 | for (int i = 0; i < Entries.Length; i++) { 42 | Entry entry = Entries[i]; 43 | writer.WriteNullTerminatedString(entry.Name); 44 | writer.Write(entry.Version.Major); 45 | writer.Write(entry.Version.Minor); 46 | writer.Write(entry.Version.Build); 47 | writer.Write(entry.Version.Revision); 48 | } 49 | } 50 | 51 | public class Entry { 52 | public string Name; 53 | public Version Version; 54 | } 55 | 56 | public object Clone() 57 | => new ChunkRListMods { 58 | Entries = Entries 59 | }; 60 | 61 | public static implicit operator ChunkRListMods(GhostNetFrame frame) 62 | => frame.Get(); 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkUActionCollision.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Update chunk sent on (best case) each "collision" frame. 17 | /// A player always receives this with a HHead.PlayerID of the "colliding" player. 18 | /// 19 | public class ChunkUActionCollision : IChunk { 20 | 21 | public const string ChunkID = "nUaC"; 22 | 23 | public bool IsValid => true; 24 | public bool IsSendable => true; 25 | 26 | public uint With; 27 | public bool Head; 28 | 29 | public void Read(BinaryReader reader) { 30 | With = reader.ReadUInt32(); 31 | Head = reader.ReadBoolean(); 32 | } 33 | 34 | public void Write(BinaryWriter writer) { 35 | writer.Write(With); 36 | writer.Write(Head); 37 | } 38 | 39 | public object Clone() 40 | => new ChunkUActionCollision { 41 | With = With, 42 | Head = Head 43 | }; 44 | 45 | public static implicit operator ChunkUActionCollision(GhostNetFrame frame) 46 | => frame.Get(); 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkUAudioPlay.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Update chunk sent when a sound should be played. 17 | /// The server can ignore or even replace a client's position. 18 | /// 19 | public class ChunkUAudioPlay : IChunk { 20 | 21 | public const string ChunkID = "nUAP"; 22 | 23 | public bool IsValid => !string.IsNullOrEmpty(Sound); 24 | public bool IsSendable => true; 25 | 26 | public string Sound; 27 | public string Param; 28 | public float Value; 29 | 30 | public Vector2? Position; 31 | 32 | public void Read(BinaryReader reader) { 33 | Sound = reader.ReadNullTerminatedString(); 34 | Param = reader.ReadNullTerminatedString(); 35 | if (!string.IsNullOrEmpty(Param)) { 36 | Value = reader.ReadSingle(); 37 | } 38 | 39 | if (reader.ReadBoolean()) 40 | Position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 41 | } 42 | 43 | public void Write(BinaryWriter writer) { 44 | writer.WriteNullTerminatedString(Sound); 45 | writer.WriteNullTerminatedString(Param); 46 | if (!string.IsNullOrEmpty(Param)) { 47 | writer.Write(Value); 48 | } 49 | 50 | if (Position != null) { 51 | writer.Write(true); 52 | writer.Write(Position.Value.X); 53 | writer.Write(Position.Value.Y); 54 | 55 | } else { 56 | writer.Write(false); 57 | } 58 | } 59 | 60 | public object Clone() 61 | => new ChunkUAudioPlay { 62 | Sound = Sound, 63 | Param = Param, 64 | Value = Value, 65 | 66 | Position = Position 67 | }; 68 | 69 | public static implicit operator ChunkUAudioPlay(GhostNetFrame frame) 70 | => frame.Get(); 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkUParticles.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Update chunk sent to emit particles. 17 | /// 18 | public class ChunkUParticles : IChunk { 19 | 20 | public const string ChunkID = "nUP"; 21 | 22 | public bool IsValid => Type != null && Type.GetID() != -1; 23 | public bool IsSendable => true; 24 | 25 | public Systems System; 26 | 27 | public ParticleType Type; 28 | 29 | public int Amount; 30 | public Vector2 Position; 31 | public Vector2 PositionRange; 32 | public Color Color; 33 | public float Direction; 34 | 35 | public void Read(BinaryReader reader) { 36 | Type = GhostNetParticleHelper.GetType(reader.ReadInt32()); 37 | 38 | System = (Systems) reader.ReadByte(); 39 | 40 | Amount = reader.ReadInt32(); 41 | Position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 42 | PositionRange = new Vector2(reader.ReadSingle(), reader.ReadSingle()); 43 | Color = new Color(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); 44 | Direction = reader.ReadSingle(); 45 | } 46 | 47 | public void Write(BinaryWriter writer) { 48 | writer.Write(Type.GetID()); 49 | 50 | writer.Write((byte) System); 51 | 52 | writer.Write(Amount); 53 | writer.Write(Position.X); 54 | writer.Write(Position.X); 55 | writer.Write(PositionRange.X); 56 | writer.Write(PositionRange.Y); 57 | writer.Write(Color.R); 58 | writer.Write(Color.G); 59 | writer.Write(Color.B); 60 | writer.Write(Color.A); 61 | writer.Write(Direction); 62 | } 63 | 64 | public object Clone() 65 | => new ChunkUParticles { 66 | System = System, 67 | 68 | Type = Type, 69 | 70 | Amount = Amount, 71 | Position = Position, 72 | PositionRange = PositionRange, 73 | Direction = Direction 74 | }; 75 | 76 | public static implicit operator ChunkUParticles(GhostNetFrame frame) 77 | => frame.Get(); 78 | 79 | public enum Systems { 80 | Particles, 81 | ParticlesBG, 82 | ParticlesFG 83 | } 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/ChunkUUpdate.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | [Chunk(ChunkID)] 15 | /// 16 | /// Update chunk sent on (best case) each frame. 17 | /// If the player receives this with their own player ID, the server is moving the player. 18 | /// 19 | public class ChunkUUpdate : IChunk { 20 | 21 | public const string ChunkID = "nU"; 22 | 23 | public bool IsValid => true; 24 | public bool IsSendable => true; 25 | 26 | public uint UpdateIndex; 27 | public GhostChunkData Data; 28 | 29 | public Color[] HairColors; 30 | public string[] HairTextures; 31 | 32 | public void Read(BinaryReader reader) { 33 | UpdateIndex = reader.ReadUInt32(); 34 | Data.Read(reader, int.MaxValue); 35 | 36 | HairColors = new Color[reader.ReadByte()]; 37 | for (int i = 0; i < HairColors.Length; i++) { 38 | HairColors[i] = new Color(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); 39 | } 40 | 41 | HairTextures = new string[reader.ReadByte()]; 42 | for (int i = 0; i < HairColors.Length; i++) { 43 | HairTextures[i] = reader.ReadNullTerminatedString(); 44 | if (HairTextures[i] == "-") 45 | HairTextures[i] = HairTextures[i - 1]; 46 | } 47 | } 48 | 49 | public void Write(BinaryWriter writer) { 50 | writer.Write(UpdateIndex); 51 | Data.Write(writer); 52 | 53 | if (HairColors == null || HairColors.Length == 0) { 54 | writer.Write((byte) 0); 55 | } else { 56 | writer.Write((byte) HairColors.Length); 57 | for (int i = 0; i < HairColors.Length; i++) { 58 | writer.Write(HairColors[i].R); 59 | writer.Write(HairColors[i].G); 60 | writer.Write(HairColors[i].B); 61 | writer.Write(HairColors[i].A); 62 | } 63 | } 64 | 65 | if (HairTextures == null || HairTextures.Length == 0) { 66 | writer.Write((byte) 0); 67 | } else { 68 | writer.Write((byte) HairTextures.Length); 69 | for (int i = 0; i < HairTextures.Length; i++) { 70 | if (i > 1 && HairTextures[i] == HairTextures[i - 1]) 71 | writer.WriteNullTerminatedString("-"); 72 | else 73 | writer.WriteNullTerminatedString(HairTextures[i]); 74 | } 75 | } 76 | } 77 | 78 | public object Clone() 79 | => new ChunkUUpdate { 80 | UpdateIndex = UpdateIndex, 81 | Data = Data, 82 | 83 | HairColors = new List(HairColors).ToArray(), 84 | HairTextures = new List(HairTextures).ToArray() 85 | }; 86 | 87 | public static implicit operator ChunkUUpdate(GhostNetFrame frame) 88 | => frame.Get(); 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /GhostNetMod/Chunks/IChunk.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using YamlDotNet.Serialization; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | public interface IChunk : ICloneable { 15 | 16 | bool IsValid { get; } 17 | bool IsSendable { get; } 18 | 19 | void Read(BinaryReader reader); 20 | 21 | void Write(BinaryWriter writer); 22 | 23 | } 24 | public class ChunkAttribute : Attribute { 25 | 26 | public string ID; 27 | 28 | public ChunkAttribute(string id) { 29 | ID = id; 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GhostNetMod/Connection/GhostNetConnection.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Net.Sockets; 11 | using System.Reflection; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | namespace Celeste.Mod.Ghost.Net { 17 | public abstract class GhostNetConnection : IDisposable { 18 | 19 | public string Context; 20 | 21 | public IPEndPoint ManagementEndPoint; 22 | public IPEndPoint UpdateEndPoint; 23 | 24 | public Action OnReceiveManagement; 25 | public Action OnReceiveUpdate; 26 | public Action OnDisconnect; 27 | 28 | public GhostNetConnection() { 29 | // Get the context in which the connection was created. 30 | StackTrace trace = new StackTrace(); 31 | foreach (StackFrame frame in trace.GetFrames()) { 32 | MethodBase method = frame.GetMethod(); 33 | if (method.IsConstructor) 34 | continue; 35 | 36 | Context = method.DeclaringType?.Name; 37 | Context = (Context == null ? "" : Context + "::") + method.Name; 38 | break; 39 | } 40 | } 41 | 42 | public abstract void SendManagement(GhostNetFrame frame, bool release); 43 | 44 | public abstract void SendUpdate(GhostNetFrame frame, bool release); 45 | 46 | public abstract void SendUpdate(GhostNetFrame frame, IPEndPoint remote, bool release); 47 | 48 | protected virtual void ReceiveManagement(IPEndPoint remote, GhostNetFrame frame) { 49 | ManagementEndPoint = remote; 50 | try { 51 | OnReceiveManagement?.Invoke(this, remote, frame); 52 | } catch (Exception e) { 53 | Logger.Log(LogLevel.Warn, "ghostnet-con", "Failed handling management frame"); 54 | LogContext(LogLevel.Warn); 55 | e.LogDetailed(); 56 | } 57 | } 58 | 59 | protected virtual void ReceiveUpdate(IPEndPoint remote, GhostNetFrame frame) { 60 | UpdateEndPoint = remote; 61 | try { 62 | OnReceiveUpdate?.Invoke(this, remote, frame); 63 | } catch (Exception e) { 64 | Logger.Log(LogLevel.Warn, "ghostnet-con", "Failed handling update frame"); 65 | LogContext(LogLevel.Warn); 66 | e.LogDetailed(); 67 | } 68 | } 69 | 70 | public void LogContext(LogLevel level) { 71 | Logger.Log(level, "ghostnet-con", $"Context: {Context} {ManagementEndPoint} {UpdateEndPoint}"); 72 | } 73 | 74 | protected virtual void Dispose(bool disposing) { 75 | OnDisconnect?.Invoke(this); 76 | } 77 | 78 | public void Dispose() { 79 | Dispose(true); 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /GhostNetMod/Connection/GhostNetLocalConnection.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Net.Sockets; 11 | using System.Reflection; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | namespace Celeste.Mod.Ghost.Net { 17 | public class GhostNetLocalConnection : GhostNetConnection { 18 | 19 | public GhostNetLocalConnection() 20 | : base() { 21 | ManagementEndPoint = new IPEndPoint(IPAddress.Loopback, 0); 22 | UpdateEndPoint = new IPEndPoint(IPAddress.Loopback, 0); 23 | } 24 | 25 | public override void SendManagement(GhostNetFrame frame, bool release) { 26 | ReceiveManagement(ManagementEndPoint, (GhostNetFrame) frame.Clone()); 27 | } 28 | 29 | public override void SendUpdate(GhostNetFrame frame, bool release) { 30 | ReceiveUpdate(UpdateEndPoint, (GhostNetFrame) frame.Clone()); 31 | } 32 | 33 | public override void SendUpdate(GhostNetFrame frame, IPEndPoint remote, bool release) { 34 | throw new NotSupportedException("Local connections don't support sending updates to another client."); 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GhostNetMod/Content/Dialog/English.txt: -------------------------------------------------------------------------------- 1 | # NOTES: 2 | # The # Symbol at the start of a line counts as a Comment. To include in dialog, use a \# 3 | # The . Symbol will cause a pause unless escaped with \. (ex: Mr. Oshiro has a pause, Mr\. Oshiro does not) 4 | # Newlines automatically create a Page Break, unless there is an {n} command on the previous line 5 | # Commands: Anything inside of curly braces {...} is a command and should not be translated. 6 | 7 | # Inline Text Commands: 8 | # {~}wavy text{/~} 9 | # {!}impact text{/!} 10 | # {>> x}changes speed at which characters are displayed{>>} 11 | # {# 000000}this text is black{#} (uses HEX color values) 12 | # {+MENU_BEGIN} inserts the dialog from the MENU_BEGIN value (in English, "CLIMB") 13 | # {n} creates a newline, without a page break 14 | # {0.5} creates a 0.5 second pause 15 | # {big}this text is large{/big} 16 | 17 | # Gameplay Control Commands (should never change) 18 | # {trigger x} this triggers an in-game event 19 | # {anchor} controls the visual position of the textbox in-game 20 | 21 | # GhostNet Module Postcards 22 | POSTCARD_GHOSTNETMODULE_BACKTOMENU= The server has sent you back to the main menu\. 23 | 24 | # GhostNet Module Options 25 | MODOPTIONS_GHOSTMODULE_OVERRIDDEN= Delete GhostNetMod.dll in mod .zip for time trials 26 | MODOPTIONS_GHOSTNETMODULE_TITLE= GhostNet - Multiplayer 27 | MODOPTIONS_GHOSTNETMODULE_DEBUGWARN= WARNING: DEBUG MODE DETECTED! 28 | MODOPTIONS_GHOSTNETMODULE_CONNECTED= Connected 29 | MODOPTIONS_GHOSTNETMODULE_SERVER= Server 30 | MODOPTIONS_GHOSTNETMODULE_COLLISION= Player Collision 31 | MODOPTIONS_GHOSTNETMODULE_SOUNDS= Player Sounds 32 | MODOPTIONS_GHOSTNETMODULE_RELOADHINT= More in ModSettings/... 33 | MODOPTIONS_GHOSTNETMODULE_RELOAD= Reload Settings 34 | -------------------------------------------------------------------------------- /GhostNetMod/Content/Graphics/Atlases/Gui/ghostnetmod/iconwheel/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/GhostNetMod/Content/Graphics/Atlases/Gui/ghostnetmod/iconwheel/bg.png -------------------------------------------------------------------------------- /GhostNetMod/Content/Graphics/Atlases/Gui/ghostnetmod/iconwheel/indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/GhostNetMod/Content/Graphics/Atlases/Gui/ghostnetmod/iconwheel/indicator.png -------------------------------------------------------------------------------- /GhostNetMod/Content/Graphics/Atlases/Gui/ghostnetmod/iconwheel/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/GhostNetMod/Content/Graphics/Atlases/Gui/ghostnetmod/iconwheel/line.png -------------------------------------------------------------------------------- /GhostNetMod/GhostNetCommand.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Monocle; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Net.Sockets; 12 | using System.Reflection; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace Celeste.Mod.Ghost.Net { 18 | public abstract class GhostNetCommand { 19 | 20 | public readonly static char[] CommandNameDelimiters = { 21 | ' ', '\n' 22 | }; 23 | 24 | public abstract string Name { get; set; } 25 | public abstract string Args { get; set; } 26 | public abstract string Help { get; set; } 27 | 28 | public virtual void Parse(GhostNetCommandEnv env) { 29 | string raw = env.Text; 30 | 31 | int index = GhostNetModule.Settings.ServerCommandPrefix.Length + Name.Length - 1; // - 1 because next space required 32 | List args = new List(); 33 | while ( 34 | index + 1 < raw.Length && 35 | (index = raw.IndexOf(' ', index + 1)) >= 0 36 | ) { 37 | int next = index + 1 < raw.Length ? raw.IndexOf(' ', index + 1) : -2; 38 | if (next < 0) next = raw.Length; 39 | 40 | int argIndex = index + 1; 41 | int argLength = next - index - 1; 42 | string argString = raw.Substring(argIndex, argLength); 43 | 44 | // + 1 because space 45 | args.Add(new GhostNetCommandArg(env).Parse(raw, argIndex, argLength)); 46 | 47 | // Parse a range 48 | if (args.Count >= 3 && 49 | args[args.Count - 3].Type == GhostNetCommandArg.EType.Int && 50 | (args[args.Count - 2].String == "-" || args[args.Count - 2].String == "+") && 51 | args[args.Count - 1].Type == GhostNetCommandArg.EType.Int 52 | ) { 53 | args.Add(new GhostNetCommandArg(env).Parse(raw, args[args.Count - 3].Index, next - args[args.Count - 3].Index)); 54 | args.RemoveRange(args.Count - 4, 3); 55 | continue; 56 | } 57 | } 58 | 59 | Run(env, args.ToArray()); 60 | } 61 | 62 | public virtual void Run(GhostNetCommandEnv env, params GhostNetCommandArg[] args) { 63 | 64 | } 65 | 66 | } 67 | 68 | public class GhostNetDCommand : GhostNetCommand { 69 | 70 | public override string Name { get; set; } 71 | public override string Args { get; set; } 72 | public override string Help { get; set; } 73 | 74 | public Action OnParse; 75 | public override void Parse(GhostNetCommandEnv env) { 76 | if (OnParse != null) { 77 | OnParse(this, env); 78 | return; 79 | } 80 | base.Parse(env); 81 | } 82 | 83 | public Action OnRun; 84 | public override void Run(GhostNetCommandEnv env, params GhostNetCommandArg[] args) 85 | => OnRun(this, env, args); 86 | 87 | public static class Parsers { 88 | /// 89 | /// Parse everything as one argument and run the command. 90 | /// 91 | public static void Everything(GhostNetCommand cmd, GhostNetCommandEnv env) 92 | => cmd.Run(env, new GhostNetCommandArg(env).Parse(env.Text, GhostNetModule.Settings.ServerCommandPrefix.Length + cmd.Name.Length + 1)); 93 | } 94 | 95 | } 96 | 97 | public class GhostNetCommandArg { 98 | 99 | public GhostNetCommandEnv Env; 100 | 101 | public string RawText; 102 | public string String; 103 | public int Index; 104 | 105 | public EType Type; 106 | 107 | public int Int; 108 | public long Long; 109 | public ulong ULong; 110 | public float Float; 111 | 112 | public int IntRangeFrom; 113 | public int IntRangeTo; 114 | public int IntRangeMin => Math.Min(IntRangeFrom, IntRangeTo); 115 | public int IntRangeMax => Math.Max(IntRangeFrom, IntRangeTo); 116 | 117 | public GhostNetConnection Connection { 118 | get { 119 | if (Type != EType.Int) 120 | throw new Exception("Argument not an ID!"); 121 | if (Int < 0 || Env.Server.Connections.Count <= Int) 122 | throw new Exception("ID out of range!"); 123 | 124 | GhostNetConnection con = Env.Server.Connections[Int]; 125 | if (con == null) 126 | throw new Exception("ID already disconnected!"); 127 | 128 | return con; 129 | } 130 | } 131 | 132 | public ChunkMPlayer Player { 133 | get { 134 | if (Type != EType.Int) 135 | throw new Exception("Argument not an ID!"); 136 | if (Int < 0 || Env.Server.Connections.Count <= Int) 137 | throw new Exception("ID out of range!"); 138 | 139 | ChunkMPlayer player; 140 | if (!Env.Server.PlayerMap.TryGetValue((uint) Int, out player) || player == null) 141 | throw new Exception("ID already disconnected!"); 142 | 143 | return player; 144 | } 145 | } 146 | 147 | public GhostNetCommandArg(GhostNetCommandEnv env) { 148 | Env = env; 149 | } 150 | 151 | public virtual GhostNetCommandArg Parse(string raw, int index) { 152 | RawText = raw; 153 | if (index < 0 || raw.Length <= index) { 154 | String = ""; 155 | Index = 0; 156 | return this; 157 | } 158 | String = raw.Substring(index); 159 | Index = index; 160 | 161 | return Parse(); 162 | } 163 | public virtual GhostNetCommandArg Parse(string raw, int index, int length) { 164 | RawText = raw; 165 | String = raw.Substring(index, length); 166 | Index = index; 167 | 168 | return Parse(); 169 | } 170 | 171 | public virtual GhostNetCommandArg Parse() { 172 | if (int.TryParse(String, out Int)) { 173 | Type = EType.Int; 174 | Long = IntRangeFrom = IntRangeTo = Int; 175 | ULong = (ulong) Int; 176 | 177 | } else if (long.TryParse(String, out Long)) { 178 | Type = EType.Long; 179 | ULong = (ulong) Long; 180 | 181 | } else if (ulong.TryParse(String, out ULong)) { 182 | Type = EType.ULong; 183 | 184 | } else if (float.TryParse(String, out Float)) { 185 | Type = EType.Float; 186 | } 187 | 188 | if (Type == EType.String) { 189 | string[] split; 190 | int from, to; 191 | if ((split = String.Split('-')).Length == 2) { 192 | if (int.TryParse(split[0].Trim(), out from) && int.TryParse(split[1].Trim(), out to)) { 193 | Type = EType.IntRange; 194 | IntRangeFrom = from; 195 | IntRangeTo = to; 196 | } 197 | } else if ((split = String.Split('+')).Length == 2) { 198 | if (int.TryParse(split[0].Trim(), out from) && int.TryParse(split[1].Trim(), out to)) { 199 | Type = EType.IntRange; 200 | IntRangeFrom = from; 201 | IntRangeTo = from + to; 202 | } 203 | } 204 | } 205 | 206 | return this; 207 | } 208 | 209 | public string Restored { 210 | get { 211 | return RawText.Substring(Index); 212 | } 213 | } 214 | 215 | public override string ToString() { 216 | return String; 217 | } 218 | 219 | public static implicit operator string(GhostNetCommandArg d) { 220 | return d.String; 221 | } 222 | 223 | public enum EType { 224 | String, 225 | 226 | Int, 227 | IntRange, 228 | 229 | Long, 230 | ULong, 231 | 232 | Float, 233 | } 234 | 235 | } 236 | 237 | public struct GhostNetCommandEnv { 238 | 239 | public GhostNetServer Server; 240 | public GhostNetConnection Connection; 241 | public GhostNetFrame Frame; 242 | 243 | public uint PlayerID => Frame.HHead.PlayerID; 244 | public bool IsOP => Server.OPs.Contains(PlayerID); 245 | 246 | public ChunkHHead HHead => Frame.HHead; 247 | 248 | public ChunkMPlayer MPlayer => Frame.MPlayer; 249 | public ChunkMChat MChat => Frame.Get(); 250 | 251 | public string Text => MChat?.Text; 252 | 253 | public ChunkMChat Send(string text, string tag = null, Color? color = null, bool fillVars = false) 254 | => Server.SendMChat(Connection, Frame, text, tag, color, fillVars); 255 | 256 | } 257 | 258 | public class GhostNetCommandFieldAttribute : Attribute { 259 | 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetCommandsStandard.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Monocle; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Net.Sockets; 12 | using System.Reflection; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace Celeste.Mod.Ghost.Net { 18 | public static class GhostNetCommandsStandard { 19 | 20 | [GhostNetCommandField] 21 | public static GhostNetCommand Help = new GhostNetDCommand { 22 | Name = "help", 23 | Args = "[page] | [command]", 24 | Help = "Get help on how to use commands.", 25 | OnRun = (cmd, env, args) => { 26 | if (args.Length == 1) { 27 | if (args[0].Type == GhostNetCommandArg.EType.Int) { 28 | env.Send(Help_GetCommandPage(env, args[0].Int)); 29 | return; 30 | } 31 | 32 | env.Send(Help_GetCommandSnippet(env, args[0].String)); 33 | return; 34 | } 35 | 36 | env.Send(Help_GetCommandPage(env, 0)); 37 | } 38 | }; 39 | 40 | [GhostNetCommandField] 41 | public static GhostNetCommand Restart = new GhostNetDCommand { 42 | Name = "restart", 43 | Help = "Trigger an automatic restart, if configured by the server host. Must be OP.", 44 | OnRun = (cmd, env, args) => { 45 | if (!env.IsOP) 46 | throw new Exception("You're not OP!"); 47 | if (args.Length != 0) 48 | throw new Exception("No arguments required!"); 49 | 50 | GhostNetWatchdog.ForceRestart(); 51 | } 52 | }; 53 | 54 | public static string Help_GetCommandPage(GhostNetCommandEnv env, int page = 0) { 55 | const int pageSize = 8; 56 | 57 | string prefix = GhostNetModule.Settings.ServerCommandPrefix; 58 | StringBuilder builder = new StringBuilder(); 59 | 60 | int pages = (int) Math.Ceiling(env.Server.Commands.Count / (float) pageSize); 61 | if (page < 0 || pages <= page) 62 | throw new Exception("Page out of range!"); 63 | 64 | for (int i = page * pageSize; i < (page + 1) * pageSize && i< env.Server.Commands.Count; i++) { 65 | GhostNetCommand cmd = env.Server.Commands[i]; 66 | builder 67 | .Append(prefix) 68 | .Append(cmd.Name) 69 | .Append(" ") 70 | .Append(cmd.Args) 71 | .AppendLine(); 72 | } 73 | 74 | builder 75 | .Append("Page ") 76 | .Append(page + 1) 77 | .Append("/") 78 | .Append(pages); 79 | 80 | return builder.ToString().Trim(); 81 | } 82 | 83 | public static string Help_GetCommandSnippet(GhostNetCommandEnv env, string cmdName) { 84 | GhostNetCommand cmd = env.Server.GetCommand(cmdName); 85 | if (cmd == null) 86 | throw new Exception($"Command {cmdName} not found!"); 87 | 88 | return Help_GetCommandSnippet(env, cmd); 89 | } 90 | 91 | public static string Help_GetCommandSnippet(GhostNetCommandEnv env, GhostNetCommand cmd) { 92 | string prefix = GhostNetModule.Settings.ServerCommandPrefix; 93 | StringBuilder builder = new StringBuilder(); 94 | 95 | builder 96 | .Append(prefix) 97 | .Append(cmd.Name) 98 | .Append(" ") 99 | .Append(cmd.Args) 100 | .AppendLine() 101 | .AppendLine(cmd.Help); 102 | 103 | return builder.ToString().Trim(); 104 | } 105 | 106 | [GhostNetCommandField] 107 | public static GhostNetCommand OP = new GhostNetDCommand { 108 | Name = "op", 109 | Args = "", 110 | Help = "OP: Make another player an OP.", 111 | OnRun = (cmd, env, args) => { 112 | if (!env.IsOP) 113 | throw new Exception("You're not OP!"); 114 | if (args.Length != 1) 115 | throw new Exception("Exactly 1 argument required!"); 116 | 117 | int id = args[0].Int; 118 | if (env.Server.OPs.Contains((uint) id)) 119 | throw new Exception($"#{id} already OP!"); 120 | 121 | env.Server.OPs.Add((uint) id); 122 | } 123 | }; 124 | 125 | [GhostNetCommandField] 126 | public static GhostNetCommand Kick = new GhostNetDCommand { 127 | Name = "kick", 128 | Args = "", 129 | Help = "OP: Kick a player from the server.", 130 | OnRun = (cmd, env, args) => { 131 | if (!env.IsOP) 132 | throw new Exception("You're not OP!"); 133 | if (args.Length != 1) 134 | throw new Exception("Exactly 1 argument required!"); 135 | 136 | int index = env.Server.OPs.IndexOf((uint) args[0].Int); 137 | int indexSelf = env.Server.OPs.IndexOf(env.PlayerID); 138 | if (-1 < index && index < indexSelf) 139 | throw new Exception("Cannot kick a higher OP!"); 140 | 141 | GhostNetConnection other = args[0].Connection; 142 | other.Dispose(); 143 | } 144 | }; 145 | 146 | [GhostNetCommandField] 147 | public static GhostNetCommand Broadcast = new GhostNetDCommand { 148 | Name = "broadcast", 149 | Args = "", 150 | Help = "OP: Broadcast something as the server.", 151 | OnParse = GhostNetDCommand.Parsers.Everything, 152 | OnRun = (cmd, env, args) => { 153 | if (!env.IsOP) 154 | throw new Exception("You're not OP!"); 155 | if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0])) 156 | return; 157 | 158 | env.Server.BroadcastMChat(env.Frame, args[0], fillVars: false); 159 | } 160 | }; 161 | 162 | [GhostNetCommandField] 163 | public static GhostNetCommand Teleport = new GhostNetDCommand { 164 | Name = "tp", 165 | Args = "", 166 | Help = "Teleport to another player.", 167 | OnRun = (cmd, env, args) => { 168 | if (args.Length != 1) 169 | throw new Exception("Exactly 1 argument required!"); 170 | 171 | ChunkMPlayer other = args[0].Player; 172 | if (string.IsNullOrEmpty(other.SID)) 173 | throw new Exception("Player in menu!"); 174 | 175 | ChunkMChat msg = env.Send($"Teleporting to {other.Name}#{args[0].Int}..."); 176 | 177 | ChunkMSession session; 178 | // Request the current session information from the other player. 179 | env.Server.Request(args[0].Connection, out session); 180 | 181 | env.Connection.SendManagement(new GhostNetFrame { 182 | env.HHead, 183 | 184 | new ChunkMPlayer { 185 | Name = env.MPlayer.Name, 186 | SID = other.SID, 187 | Mode = other.Mode, 188 | Level = other.Level 189 | }, 190 | 191 | session 192 | }, true); 193 | 194 | msg.Text = $"Teleported to {other.Name}#{args[0].Int}"; 195 | env.Connection.SendManagement(new GhostNetFrame { 196 | env.HHead, 197 | msg 198 | }, true); 199 | 200 | } 201 | }; 202 | 203 | [GhostNetCommandField] 204 | public static GhostNetCommand Emote = new GhostNetDCommand { 205 | Name = "emote", 206 | Args = " | i: | p:", 207 | Help = 208 | @"Send an emote appearing over your player. 209 | Normal text appears over your player. 210 | This syntax also works for your ""favorites"" (settings file). 211 | i:TEXTURE shows TEXTURE from the GUI atlas. 212 | p:TEXTURE shows TEXTURE from the Portraits atlas. 213 | p:FRM1 FRM2 FRM3 plays an animation, 7 FPS by default. 214 | p:10 FRM1 FRM2 FRM3 plays the animation at 10 FPS.", 215 | OnParse = GhostNetDCommand.Parsers.Everything, 216 | OnRun = (cmd, env, args) => { 217 | if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0])) 218 | return; 219 | 220 | env.Server.Handle(env.Connection, new GhostNetFrame { 221 | env.HHead, 222 | new ChunkMEmote { 223 | Value = args[0] 224 | } 225 | }); 226 | } 227 | }; 228 | 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetEmote.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost.Net { 13 | public class GhostNetEmote : Entity { 14 | 15 | public static float Size = 256f; 16 | 17 | public Entity Tracking; 18 | 19 | public string Value; 20 | 21 | protected Camera Camera; 22 | 23 | public float Alpha = 1f; 24 | 25 | public bool PopIn = false; 26 | public bool FadeOut = false; 27 | public bool PopOut = false; 28 | public float AnimationTime; 29 | 30 | public bool Float = false; 31 | 32 | protected float time; 33 | 34 | protected GhostNetEmote(Entity tracking) 35 | : base(Vector2.Zero) { 36 | Tracking = tracking; 37 | 38 | Tag = TagsExt.SubHUD; 39 | } 40 | 41 | public GhostNetEmote(Entity tracking, string value) 42 | : this(tracking) { 43 | Value = value; 44 | } 45 | 46 | public override void Render() { 47 | base.Render(); 48 | 49 | float popupAlpha = 1f; 50 | float popupScale = 1f; 51 | 52 | if (Tracking?.Scene != Scene) 53 | PopOut = true; 54 | 55 | // Update can halt in the pause menu. 56 | if (PopIn || FadeOut || PopOut) { 57 | AnimationTime += Engine.RawDeltaTime; 58 | if (AnimationTime < 0.1f && PopIn) { 59 | float t = AnimationTime / 0.1f; 60 | // Pop in. 61 | popupAlpha = Ease.CubeOut(t); 62 | popupScale = Ease.ElasticOut(t); 63 | 64 | } else if (AnimationTime < 1f) { 65 | // Stay. 66 | popupAlpha = 1f; 67 | popupScale = 1f; 68 | 69 | } else if (FadeOut) { 70 | // Fade out. 71 | if (AnimationTime < 2f) { 72 | float t = AnimationTime - 1f; 73 | popupAlpha = 1f - Ease.CubeIn(t); 74 | popupScale = 1f - 0.4f * Ease.CubeIn(t); 75 | 76 | } else { 77 | // Destroy. 78 | RemoveSelf(); 79 | return; 80 | } 81 | 82 | } else if (PopOut) { 83 | // Pop out. 84 | if (AnimationTime < 1.1f) { 85 | float t = (AnimationTime - 1f) / 0.1f; 86 | popupAlpha = 1f - Ease.CubeIn(t); 87 | popupAlpha *= popupAlpha; 88 | popupScale = 1f - 0.4f * Ease.BounceIn(t); 89 | 90 | } else { 91 | // Destroy. 92 | RemoveSelf(); 93 | return; 94 | } 95 | 96 | } else { 97 | AnimationTime = 1f; 98 | } 99 | } 100 | 101 | time += Engine.RawDeltaTime; 102 | 103 | MTexture icon = null; 104 | string text = null; 105 | 106 | if (IsIcon(Value)) { 107 | icon = GetIcon(Value, time); 108 | 109 | } else { 110 | text = Value; 111 | } 112 | 113 | float alpha = Alpha * popupAlpha; 114 | 115 | if (alpha <= 0f || (icon == null && string.IsNullOrWhiteSpace(text))) 116 | return; 117 | 118 | if (Tracking == null) 119 | return; 120 | 121 | Level level = SceneAs(); 122 | if (level == null) 123 | return; 124 | 125 | if (Camera == null) 126 | Camera = level.Camera; 127 | if (Camera == null) 128 | return; 129 | 130 | Vector2 pos = Tracking.Position; 131 | // - name offset - popup offset 132 | pos.Y -= 16f + 4f; 133 | 134 | pos -= level.Camera.Position; 135 | pos *= 6f; // 1920 / 320 136 | 137 | if (Float) 138 | pos.Y -= (float) Math.Sin(time * 2f) * 4f; 139 | 140 | if (icon != null) { 141 | Vector2 size = new Vector2(icon.Width, icon.Height); 142 | float scale = (Size / Math.Max(size.X, size.Y)) * 0.5f * popupScale; 143 | size *= scale; 144 | 145 | pos = pos.Clamp( 146 | 0f + size.X * 0.5f, 0f + size.Y * 1f, 147 | 1920f - size.X * 0.5f, 1080f 148 | ); 149 | 150 | icon.DrawJustified( 151 | pos, 152 | new Vector2(0.5f, 1f), 153 | Color.White * alpha, 154 | Vector2.One * scale 155 | ); 156 | 157 | } else { 158 | Vector2 size = ActiveFont.Measure(text); 159 | float scale = (Size / Math.Max(size.X, size.Y)) * 0.5f * popupScale; 160 | size *= scale; 161 | 162 | pos = pos.Clamp( 163 | 0f + size.X * 0.5f, 0f + size.Y * 1f, 164 | 1920f - size.X * 0.5f, 1080f 165 | ); 166 | 167 | ActiveFont.DrawOutline( 168 | text, 169 | pos, 170 | new Vector2(0.5f, 1f), 171 | Vector2.One * scale, 172 | Color.White * alpha, 173 | 2f, 174 | Color.Black * alpha * alpha * alpha 175 | ); 176 | } 177 | } 178 | 179 | public static bool IsText(string emote) { 180 | return !IsIcon(emote); 181 | } 182 | 183 | public static bool IsIcon(string emote) { 184 | return GetIconAtlas(ref emote) != null; 185 | } 186 | 187 | private static Atlas FallbackIconAtlas = new Atlas(); 188 | public static Atlas GetIconAtlas(ref string emote) { 189 | if (emote.StartsWith("i:")) { 190 | emote = emote.Substring(2); 191 | return GFX.Gui ?? FallbackIconAtlas; 192 | } 193 | 194 | if (emote.StartsWith("g:")) { 195 | emote = emote.Substring(2); 196 | return GFX.Game ?? FallbackIconAtlas; 197 | } 198 | 199 | if (emote.StartsWith("p:")) { 200 | emote = emote.Substring(2); 201 | return GFX.Portraits ?? FallbackIconAtlas; 202 | } 203 | 204 | return null; 205 | } 206 | 207 | public static MTexture GetIcon(string emote, float time) { 208 | Atlas atlas; 209 | if ((atlas = GetIconAtlas(ref emote)) == null) 210 | return null; 211 | 212 | List iconPaths = new List(emote.Split(' ')); 213 | int fps; 214 | if (iconPaths.Count > 1 && int.TryParse(iconPaths[0], out fps)) { 215 | iconPaths.RemoveAt(0); 216 | } else { 217 | fps = 7; // Default FPS. 218 | } 219 | 220 | List icons = iconPaths.SelectMany(iconPath => { 221 | iconPath = iconPath.Trim(); 222 | List subs = atlas.GetAtlasSubtextures(iconPath); 223 | if (subs.Count != 0) 224 | return subs; 225 | if (atlas.Has(iconPath)) 226 | return new List() { atlas[iconPath] }; 227 | if (iconPath.ToLowerInvariant() == "end") 228 | return new List() { null }; 229 | return new List(); 230 | }).ToList(); 231 | 232 | if (icons.Count == 0) 233 | return null; 234 | 235 | int index = (int) Math.Floor(time * fps); 236 | 237 | if (index >= icons.Count - 1 && icons[icons.Count - 1] == null) 238 | return icons[icons.Count - 2]; 239 | 240 | return icons[index % icons.Count]; 241 | } 242 | 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetEmoteWheel.cs: -------------------------------------------------------------------------------- 1 | using FMOD.Studio; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using YamlDotNet.Serialization; 11 | 12 | namespace Celeste.Mod.Ghost.Net { 13 | public class GhostNetEmoteWheel : Entity { 14 | 15 | public Entity Tracking; 16 | 17 | protected Camera Camera; 18 | 19 | public float Alpha = 1f; 20 | 21 | protected float time = 0f; 22 | 23 | public bool Shown = false; 24 | protected bool popupShown = false; 25 | protected float popupTime = 100f; 26 | protected bool timeRateSet = false; 27 | 28 | public float Angle = 0f; 29 | 30 | public int Selected = -1; 31 | protected int PrevSelected; 32 | protected float selectedTime = 0f; 33 | 34 | public MTexture BG = GFX.Gui["ghostnetmod/iconwheel/bg"]; 35 | public MTexture Line = GFX.Gui["ghostnetmod/iconwheel/line"]; 36 | public MTexture Indicator = GFX.Gui["ghostnetmod/iconwheel/indicator"]; 37 | 38 | public Color TextSelectColorA = Calc.HexToColor("84FF54"); 39 | public Color TextSelectColorB = Calc.HexToColor("FCFF59"); 40 | 41 | public GhostNetEmoteWheel(Entity tracking) 42 | : base(Vector2.Zero) { 43 | Tracking = tracking; 44 | 45 | Tag = TagsExt.SubHUD; 46 | Depth = -1; 47 | } 48 | 49 | public override void Update() { 50 | // Update only runs while the level is "alive" (scene not paused or frozen). 51 | 52 | if (Shown && !timeRateSet) { 53 | Engine.TimeRate = 0.25f; 54 | timeRateSet = true; 55 | 56 | } else if (!Shown && timeRateSet) { 57 | Engine.TimeRate = 1f; 58 | timeRateSet = false; 59 | } 60 | 61 | base.Update(); 62 | } 63 | 64 | public override void Render() { 65 | base.Render(); 66 | 67 | string[] emotes = GhostNetModule.Settings.EmoteFavs; 68 | 69 | // Update can halt in the pause menu. 70 | 71 | if (Shown) { 72 | Angle = GhostNetModule.Instance.JoystickEmoteWheel.Value.Angle(); 73 | float angle = (float) ((Angle + Math.PI * 2f) % (Math.PI * 2f)); 74 | float start = (-0.5f / emotes.Length) * 2f * (float) Math.PI; 75 | if (2f * (float) Math.PI + start < angle) { 76 | // Angle should be start < angle < 0, but is (TAU + start) < angle < TAU 77 | angle -= 2f * (float) Math.PI; 78 | } 79 | for (int i = 0; i < emotes.Length; i++) { 80 | float min = ((i - 0.5f) / emotes.Length) * 2f * (float) Math.PI; 81 | float max = ((i + 0.5f) / emotes.Length) * 2f * (float) Math.PI; 82 | if (min <= angle && angle <= max) { 83 | Selected = i; 84 | break; 85 | } 86 | } 87 | } 88 | 89 | time += Engine.RawDeltaTime; 90 | 91 | if (!Shown) { 92 | Selected = -1; 93 | } 94 | selectedTime += Engine.RawDeltaTime; 95 | if (PrevSelected != Selected) { 96 | selectedTime = 0f; 97 | PrevSelected = Selected; 98 | } 99 | 100 | float popupAlpha; 101 | float popupScale; 102 | 103 | popupTime += Engine.RawDeltaTime; 104 | if (Shown && !popupShown) { 105 | popupTime = 0f; 106 | } else if ((Shown && popupTime > 1f) || 107 | (!Shown && popupTime < 1f)) { 108 | popupTime = 1f; 109 | } 110 | popupShown = Shown; 111 | 112 | if (popupTime < 0.1f) { 113 | float t = popupTime / 0.1f; 114 | // Pop in. 115 | popupAlpha = Ease.CubeOut(t); 116 | popupScale = Ease.ElasticOut(t); 117 | 118 | } else if (popupTime < 1f) { 119 | // Stay. 120 | popupAlpha = 1f; 121 | popupScale = 1f; 122 | 123 | } else { 124 | float t = (popupTime - 1f) / 0.2f; 125 | // Fade out. 126 | popupAlpha = 1f - Ease.CubeIn(t); 127 | popupScale = 1f - 0.2f * Ease.CubeIn(t); 128 | } 129 | 130 | float alpha = Alpha * popupAlpha; 131 | 132 | if (alpha <= 0f) 133 | return; 134 | 135 | if (Tracking == null) 136 | return; 137 | 138 | Level level = SceneAs(); 139 | if (level == null) 140 | return; 141 | 142 | if (Camera == null) 143 | Camera = level.Camera; 144 | if (Camera == null) 145 | return; 146 | 147 | Vector2 pos = Tracking.Position; 148 | pos.Y -= 8f; 149 | 150 | pos -= level.Camera.Position; 151 | pos *= 6f; // 1920 / 320 152 | 153 | float radius = BG.Width * 0.5f * 0.75f * popupScale; 154 | 155 | pos = pos.Clamp( 156 | 0f + radius, 0f + radius, 157 | 1920f - radius, 1080f - radius 158 | ); 159 | 160 | // Draw.Circle(pos, radius, Color.Black * 0.8f * alpha * alpha, radius * 0.6f * (1f + 0.2f * (float) Math.Sin(time)), 8); 161 | BG.DrawCentered( 162 | pos, 163 | Color.White * alpha * alpha * alpha, 164 | Vector2.One * popupScale 165 | ); 166 | 167 | Indicator.DrawCentered( 168 | pos, 169 | Color.White * alpha * alpha * alpha, 170 | Vector2.One * popupScale, 171 | Angle 172 | ); 173 | 174 | float selectedScale = 1.2f - 0.2f * Calc.Clamp(Ease.CubeOut(selectedTime / 0.1f), 0f, 1f) + (float) Math.Sin(time * 1.8f) * 0.05f; 175 | 176 | for (int i = 0; i < emotes.Length; i++) { 177 | Line.DrawCentered( 178 | pos, 179 | Color.White * alpha * alpha * alpha, 180 | Vector2.One * popupScale, 181 | ((i + 0.5f) / emotes.Length) * 2f * (float) Math.PI 182 | ); 183 | 184 | string emote = emotes[i]; 185 | if (string.IsNullOrEmpty(emote)) 186 | continue; 187 | 188 | float a = (i / (float) emotes.Length) * 2f * (float) Math.PI; 189 | Vector2 emotePos = pos + new Vector2( 190 | (float) Math.Cos(a), 191 | (float) Math.Sin(a) 192 | ) * radius; 193 | 194 | if (GhostNetEmote.IsIcon(emote)) { 195 | MTexture icon = GhostNetEmote.GetIcon(emote, Selected == i ? selectedTime : 0f); 196 | if (icon == null) 197 | continue; 198 | 199 | Vector2 iconSize = new Vector2(icon.Width, icon.Height); 200 | float iconScale = (GhostNetEmote.Size / Math.Max(iconSize.X, iconSize.Y)) * 0.24f * popupScale; 201 | 202 | icon.DrawCentered( 203 | emotePos, 204 | Color.White * (Selected == i ? (Calc.BetweenInterval(selectedTime, 0.1f) ? 0.9f : 1f) : 0.7f) * alpha, 205 | Vector2.One * (Selected == i ? selectedScale : 1f) * iconScale 206 | ); 207 | 208 | } else { 209 | Vector2 textSize = ActiveFont.Measure(emote); 210 | float textScale = (GhostNetEmote.Size / Math.Max(textSize.X, textSize.Y)) * 0.24f * popupScale; 211 | 212 | ActiveFont.DrawOutline( 213 | emote, 214 | emotePos, 215 | new Vector2(0.5f, 0.5f), 216 | Vector2.One * (Selected == i ? selectedScale : 1f) * textScale, 217 | (Selected == i ? (Calc.BetweenInterval(selectedTime, 0.1f) ? TextSelectColorA : TextSelectColorB) : Color.LightSlateGray) * alpha, 218 | 2f, 219 | Color.Black * alpha * alpha * alpha 220 | ); 221 | } 222 | } 223 | } 224 | 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetExtensions.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace Celeste.Mod.Ghost.Net { 14 | public static class GhostNetExtensions { 15 | 16 | private readonly static FieldInfo f_TrailManager_shapshots = typeof(TrailManager).GetField("snapshots", BindingFlags.NonPublic | BindingFlags.Instance); 17 | 18 | public static TrailManager.Snapshot[] GetSnapshots(this TrailManager self) 19 | => (TrailManager.Snapshot[]) f_TrailManager_shapshots.GetValue(self); 20 | 21 | public static string Nullify(this string value) 22 | => string.IsNullOrEmpty(value) ? null : value; 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetFrame.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod.Helpers; 2 | using FMOD.Studio; 3 | using Microsoft.Xna.Framework; 4 | using Monocle; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using YamlDotNet.Serialization; 14 | 15 | namespace Celeste.Mod.Ghost.Net { 16 | public delegate void GhostNetFrameHandler(GhostNetConnection con, GhostNetFrame frame); 17 | public delegate IChunk GhostNetChunkParser(BinaryReader reader); 18 | /// 19 | /// A GhostNetFrame is a collection of many individual Chunks, which can have an individual or combined meaning. 20 | /// 21 | public sealed class GhostNetFrame : ICloneable, IEnumerable { 22 | 23 | private static IDictionary ChunkParsers = new Dictionary(); 24 | private static IDictionary ChunkIDs = new Dictionary(); 25 | 26 | // TODO: Wrapper for objects that don't natively implement IChunk, but implement its members. 27 | 28 | public static void RegisterChunk(Type type, string id, Func parser) 29 | => RegisterChunk(type, id, reader => parser(reader) as IChunk); 30 | public static void RegisterChunk(Type type, string id, GhostNetChunkParser parser) { 31 | ChunkIDs[type] = id; 32 | ChunkParsers[id] = parser; 33 | } 34 | 35 | public static string GetChunkID(Type type) { 36 | string id; 37 | if (ChunkIDs.TryGetValue(type, out id)) 38 | return id; 39 | ChunkAttribute chunkInfo = type.GetCustomAttribute(); 40 | if (chunkInfo != null) 41 | return ChunkIDs[type] = chunkInfo.ID; 42 | throw new InvalidDataException("Unregistered chunk type"); 43 | } 44 | 45 | public static void RegisterChunksFromModule(EverestModule module) { 46 | foreach (Type type in module.GetType().Assembly.GetTypes()) { 47 | ChunkAttribute chunkInfo = type.GetCustomAttribute(); 48 | if (chunkInfo == null) 49 | continue; 50 | // TODO: Can be optimized. Who wants to write a DynamicMethod generator for this? :^) 51 | RegisterChunk(type, chunkInfo.ID, reader => { 52 | IChunk chunk = (IChunk) Activator.CreateInstance(type); 53 | chunk.Read(reader); 54 | return chunk; 55 | }); 56 | } 57 | } 58 | 59 | /// 60 | /// Server-internal field. Should the frame be propagated after handling? 61 | /// 62 | public bool PropagateM; 63 | /// 64 | /// Server-internal field. Should the frame be propagated after handling? 65 | /// 66 | public bool PropagateU; 67 | 68 | public IDictionary ChunkMap = new Dictionary(); 69 | 70 | #region Standard Chunks 71 | 72 | // Head chunk, added by server. 73 | public ChunkHHead HHead { 74 | get { 75 | return Get(); 76 | } 77 | set { 78 | Add(value); 79 | } 80 | } 81 | 82 | public ChunkMPlayer MPlayer { 83 | get { 84 | return Get(); 85 | } 86 | set { 87 | Add(value); 88 | } 89 | } 90 | public ChunkUUpdate UUpdate { 91 | get { 92 | return Get(); 93 | } 94 | set { 95 | Add(value); 96 | } 97 | } 98 | 99 | #endregion 100 | 101 | // Unparsed chunks, modifyable by mods. 102 | public byte[] Extra; 103 | 104 | public void Read(BinaryReader reader) { 105 | string id; 106 | // The last "chunk" type, \r\n (Windows linebreak), doesn't contain a length. 107 | using (MemoryStream extraBuffer = new MemoryStream()) 108 | using (BinaryWriter extraWriter = new BinaryWriter(extraBuffer)) { 109 | while ((id = reader.ReadNullTerminatedString()) != "\r\n") { 110 | uint length = reader.ReadUInt32(); 111 | GhostNetChunkParser parser; 112 | if (ChunkParsers.TryGetValue(id, out parser)) { 113 | IChunk chunk = parser(reader); 114 | if (chunk != null && chunk.IsValid) { 115 | lock (ChunkMap) { 116 | ChunkMap[chunk.GetType()] = chunk; 117 | } 118 | } 119 | 120 | } else { 121 | // Store any unknown chunks. 122 | extraWriter.WriteNullTerminatedString(id); 123 | extraWriter.Write(length); 124 | extraWriter.Write(reader.ReadBytes((int) length)); 125 | break; 126 | } 127 | } 128 | 129 | extraWriter.Flush(); 130 | Extra = extraBuffer.ToArray(); 131 | } 132 | } 133 | 134 | public void Write(BinaryWriter writer) { 135 | lock (ChunkMap) { 136 | foreach (IChunk chunk in ChunkMap.Values) 137 | if (chunk != null && chunk.IsValid && chunk.IsSendable) 138 | GhostFrame.WriteChunk(writer, chunk.Write, GetChunkID(chunk.GetType())); 139 | } 140 | 141 | if (Extra != null) 142 | writer.Write(Extra); 143 | 144 | writer.WriteNullTerminatedString(GhostFrame.End); 145 | } 146 | 147 | public GhostNetFrame Add(T chunk) where T : IChunk 148 | => Add(typeof(T), chunk); 149 | public GhostNetFrame Add(Type t, IChunk chunk) { 150 | // Assume that chunk is t for performance reasons. 151 | lock (ChunkMap) { 152 | ChunkMap[t] = chunk; 153 | } 154 | return this; 155 | } 156 | 157 | public void Remove() where T : IChunk 158 | => Remove(typeof(T)); 159 | public void Remove(Type t) { 160 | lock (ChunkMap) { 161 | ChunkMap[t] = null; 162 | } 163 | } 164 | 165 | public void Get(out T chunk) where T : IChunk 166 | => chunk = (T) Get(typeof(T)); 167 | public T Get() where T : IChunk 168 | => (T) Get(typeof(T)); 169 | public IChunk Get(Type t) { 170 | IChunk chunk; 171 | if (ChunkMap.TryGetValue(t, out chunk) && chunk != null && chunk.IsValid) 172 | return chunk; 173 | return null; 174 | } 175 | 176 | public bool Has() where T : IChunk 177 | => Has(typeof(T)); 178 | public bool Has(Type t) 179 | => Get(t) != null; 180 | 181 | public object Clone() { 182 | GhostNetFrame clone = new GhostNetFrame(); 183 | lock (ChunkMap) { 184 | foreach (KeyValuePair entry in ChunkMap) 185 | if (entry.Value != null && entry.Value.IsValid && entry.Value.IsSendable) 186 | clone.ChunkMap[entry.Key] = (IChunk) entry.Value.Clone(); 187 | } 188 | return clone; 189 | } 190 | 191 | public IEnumerator GetEnumerator() { 192 | return ChunkMap.Values.GetEnumerator(); 193 | } 194 | 195 | IEnumerator IEnumerable.GetEnumerator() { 196 | return ChunkMap.Values.GetEnumerator(); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetHooks.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using FMOD.Studio; 12 | using Microsoft.Xna.Framework.Input; 13 | 14 | namespace Celeste.Mod.Ghost.Net { 15 | public static class GhostNetHooks { 16 | 17 | public static void Load() { 18 | On.Celeste.PlayerHair.GetHairColor += GetHairColor; 19 | On.Celeste.PlayerHair.GetHairTexture += GetHairTexture; 20 | On.Celeste.Player.Play += PlayerPlayAudio; 21 | } 22 | 23 | public static Color GetHairColor(On.Celeste.PlayerHair.orig_GetHairColor orig, PlayerHair self, int index) { 24 | Color colorOrig = orig(self, index); 25 | Ghost ghost = self.Entity as Ghost; 26 | GhostNetClient client = GhostNetModule.Instance.Client; 27 | uint playerID; 28 | GhostNetFrame frame; 29 | ChunkUUpdate update; 30 | if (ghost == null || 31 | client == null || 32 | !client.GhostPlayerIDs.TryGetValue(ghost, out playerID) || 33 | !client.UpdateMap.TryGetValue(playerID, out frame) || 34 | (update = frame) == null) 35 | return colorOrig; 36 | 37 | if (index < 0 || update.HairColors.Length <= index) 38 | return Color.Transparent; 39 | return update.HairColors[index]; 40 | } 41 | 42 | public static MTexture GetHairTexture(On.Celeste.PlayerHair.orig_GetHairTexture orig, PlayerHair self, int index) { 43 | MTexture texOrig = orig(self, index); 44 | Ghost ghost = self.Entity as Ghost; 45 | GhostNetClient client = GhostNetModule.Instance.Client; 46 | uint playerID; 47 | GhostNetFrame frame; 48 | ChunkUUpdate update; 49 | if (ghost == null || 50 | client == null || 51 | !client.GhostPlayerIDs.TryGetValue(ghost, out playerID) || 52 | !client.UpdateMap.TryGetValue(playerID, out frame) || 53 | (update = frame) == null) 54 | return texOrig; 55 | 56 | if (index < 0 || update.HairColors.Length <= index) 57 | return texOrig; 58 | string texName = update.HairTextures[index]; 59 | if (!GFX.Game.Has(texName)) 60 | return texOrig; 61 | return GFX.Game[texName]; 62 | } 63 | 64 | public static EventInstance PlayerPlayAudio(On.Celeste.Player.orig_Play orig, Player self, string sound, string param, float value) { 65 | GhostNetModule.Instance?.Client?.SendUAudio(self, sound, param, value); 66 | return orig(self, sound, param, value); 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetMod.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {2F04DF2A-9512-41A4-B981-2B86A31E4E45} 8 | Library 9 | Properties 10 | Celeste.Mod.Ghost.Net 11 | GhostNetMod 12 | v4.5.2 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\Everest\lib-stripped\Celeste.exe 35 | False 36 | 37 | 38 | ..\Everest\lib-stripped\FNA.dll 39 | False 40 | 41 | 42 | ..\deps\MMHOOK_Celeste.dll 43 | False 44 | 45 | 46 | ..\deps\Tmds.Systemd.dll 47 | 48 | 49 | ..\Everest\lib-stripped\Steamworks.NET.dll 50 | False 51 | 52 | 53 | 54 | 55 | ..\Everest\lib\YamlDotNet.dll 56 | False 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {d5d0239d-ff95-4897-9484-1898ab7e82f5} 98 | Celeste.Mod.mm 99 | False 100 | 101 | 102 | {4abf8c07-c533-407e-9ec9-534c2916c907} 103 | GhostMod 104 | False 105 | 106 | 107 | 108 | 109 | Content\Dialog\English.txt 110 | 111 | 112 | Content\Graphics\Atlases\Gui\ghostnetmod\iconwheel\bg.png 113 | 114 | 115 | Content\Graphics\Atlases\Gui\ghostnetmod\iconwheel\line.png 116 | 117 | 118 | Content\Graphics\Atlases\Gui\ghostnetmod\iconwheel\indicator.png 119 | 120 | 121 | 122 | 123 | everest.yaml 124 | PreserveNewest 125 | 126 | 127 | 128 | 135 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetModule.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod; 2 | using Microsoft.Xna.Framework; 3 | using Monocle; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using FMOD.Studio; 12 | using Microsoft.Xna.Framework.Input; 13 | 14 | namespace Celeste.Mod.Ghost.Net { 15 | public class GhostNetModule : EverestModule { 16 | 17 | public static GhostNetModule Instance; 18 | 19 | public GhostNetModule() { 20 | Instance = this; 21 | } 22 | 23 | public override Type SettingsType => typeof(GhostNetModuleSettings); 24 | public static GhostNetModuleSettings Settings => (GhostNetModuleSettings) Instance._Settings; 25 | 26 | public GhostNetServer Server; 27 | public GhostNetClient Client; 28 | 29 | public VirtualButton ButtonPlayerList; 30 | public VirtualJoystick JoystickEmoteWheel; 31 | public VirtualButton ButtonEmoteSend; 32 | public VirtualButton ButtonChat; 33 | 34 | private bool _StartServer; 35 | private bool _StartHeadless; 36 | 37 | public override void LoadSettings() { 38 | base.LoadSettings(); 39 | 40 | if (Settings.EmoteFavs == null || Settings.EmoteFavs.Length == 0) { 41 | Settings.EmoteFavs = new string[] { 42 | "i:collectables/heartgem/0/spin", 43 | "i:collectables/strawberry", 44 | "Hi!", 45 | "Too slow!", 46 | "p:madeline/normal04", 47 | "p:ghost/scoff03", 48 | "p:theo/yolo03 theo/yolo02 theo/yolo01 theo/yolo02 END", 49 | "p:granny/laugh", 50 | }; 51 | } 52 | } 53 | 54 | public override void Load() { 55 | Everest.Events.Input.OnInitialize += OnInputInitialize; 56 | Everest.Events.Input.OnDeregister += OnInputDeregister; 57 | 58 | GhostNetHooks.Load(); 59 | 60 | // Example of a MP server mod. 61 | GhostNetServer.OnCreate += GhostNetRaceManager.OnCreateServer; 62 | 63 | base.Initialize(); 64 | 65 | Queue args = new Queue(Everest.Args); 66 | while (args.Count > 0) { 67 | string arg = args.Dequeue(); 68 | if (arg == "--server") { 69 | _StartServer = true; 70 | } else if (arg == "--headless") { 71 | _StartHeadless = true; 72 | } 73 | } 74 | 75 | GhostModule.SettingsOverridden = true; 76 | ResetGhostModuleSettings(); 77 | 78 | if (_StartServer && _StartHeadless) { 79 | // We don't care about other mods. 80 | GhostNetFrame.RegisterChunksFromModule(this); 81 | 82 | Start(true, true); 83 | RunDedicated(); 84 | Environment.Exit(0); 85 | } 86 | } 87 | 88 | public override void Initialize() { 89 | base.Initialize(); 90 | 91 | // Register after all mods have loaded. 92 | foreach (EverestModule module in Everest.Modules) 93 | GhostNetFrame.RegisterChunksFromModule(module); 94 | 95 | if (_StartServer && !_StartHeadless) { 96 | Start(true, true); 97 | } 98 | } 99 | 100 | public override void Unload() { 101 | Everest.Events.Input.OnInitialize -= OnInputInitialize; 102 | Everest.Events.Input.OnDeregister -= OnInputDeregister; 103 | Stop(); 104 | OnInputDeregister(); 105 | } 106 | 107 | public override void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance snapshot) { 108 | base.CreateModMenuSection(menu, inGame, snapshot); 109 | 110 | menu.Add(new TextMenu.Button("modoptions_ghostnetmodule_reloadhint".DialogCleanOrNull() ?? "More in ModSettings/...") { 111 | Disabled = true 112 | }); 113 | 114 | menu.Add(new TextMenu.Button("modoptions_ghostnetmodule_reload".DialogCleanOrNull() ?? "Reload Settings").Pressed(() => { 115 | string server = Settings.Server; 116 | LoadSettings(); 117 | if (Settings.Server != server) 118 | Settings.Server = Settings._Server; 119 | })); 120 | } 121 | 122 | public static void ResetGhostModuleSettings() { 123 | string name = GhostModule.Settings.Name; 124 | GhostModule.Instance._Settings = new GhostModuleSettings(); 125 | GhostModule.Settings.Mode = GhostModuleMode.Off; 126 | GhostModule.Settings.Name = name; 127 | GhostModule.Settings.NameFilter = ""; 128 | GhostModule.Settings.ShowNames = true; 129 | GhostModule.Settings.ShowDeaths = true; 130 | GhostModule.Settings.InnerOpacity = 8; 131 | GhostModule.Settings.InnerHairOpacity = 8; 132 | GhostModule.Settings.OuterOpacity = 8; 133 | GhostModule.Settings.OuterHairOpacity = 8; 134 | } 135 | 136 | public void OnInputInitialize() { 137 | ButtonPlayerList = new VirtualButton( 138 | new VirtualButton.KeyboardKey(Keys.Tab), 139 | new VirtualButton.PadButton(Input.Gamepad, Buttons.Back) 140 | ); 141 | AddButtonsTo(ButtonPlayerList, Settings.ButtonPlayerList); 142 | 143 | JoystickEmoteWheel = new VirtualJoystick(true, 144 | new VirtualJoystick.PadRightStick(Input.Gamepad, 0.2f) 145 | ); 146 | ButtonEmoteSend = new VirtualButton( 147 | new VirtualButton.KeyboardKey(Keys.Q), 148 | new VirtualButton.PadButton(Input.Gamepad, Buttons.RightStick) 149 | ); 150 | AddButtonsTo(ButtonEmoteSend, Settings.ButtonEmoteSend); 151 | 152 | ButtonChat = new VirtualButton( 153 | new VirtualButton.KeyboardKey(Keys.T) 154 | ); 155 | AddButtonsTo(ButtonEmoteSend, Settings.ButtonChat); 156 | } 157 | 158 | public void OnInputDeregister() { 159 | ButtonPlayerList?.Deregister(); 160 | JoystickEmoteWheel?.Deregister(); 161 | ButtonEmoteSend?.Deregister(); 162 | ButtonChat?.Deregister(); 163 | } 164 | 165 | private static void AddButtonsTo(VirtualButton vbtn, List buttons) { 166 | if (buttons == null) 167 | return; 168 | foreach (Buttons button in buttons) { 169 | if (button == Buttons.LeftTrigger) { 170 | vbtn.Nodes.Add(new VirtualButton.PadLeftTrigger(Input.Gamepad, 0.25f)); 171 | } else if (button == Buttons.RightTrigger) { 172 | vbtn.Nodes.Add(new VirtualButton.PadRightTrigger(Input.Gamepad, 0.25f)); 173 | } else { 174 | vbtn.Nodes.Add(new VirtualButton.PadButton(Input.Gamepad, button)); 175 | } 176 | } 177 | } 178 | 179 | public void Start(bool server = false, bool client = false) { 180 | Stop(); 181 | 182 | if (Settings.IsHost || server) { 183 | Server = new GhostNetServer(Celeste.Instance); 184 | if (!_StartHeadless) 185 | Celeste.Instance.Components.Add(Server); 186 | Server.OPs.Add(0); 187 | Server.Start(); 188 | GhostNetWatchdog.InitializeWatchdog(); 189 | } 190 | 191 | if (!Settings.IsHost && server && !client) 192 | return; 193 | 194 | try { 195 | Client = new GhostNetClient(Celeste.Instance); 196 | if (!_StartHeadless) 197 | Celeste.Instance.Components.Add(Client); 198 | Client.Start(); 199 | } catch (Exception e) { 200 | Logger.Log(LogLevel.Warn, "ghostnet", "Failed starting client"); 201 | e.LogDetailed(); 202 | if (Settings.EnabledEntry != null) { 203 | Settings.EnabledEntry.LeftPressed(); 204 | } 205 | Stop(); 206 | } 207 | } 208 | 209 | public void Stop() { 210 | if (Client != null) { 211 | Client.Stop(); 212 | Client = null; 213 | } 214 | 215 | if (Server != null) { 216 | Server.Stop(); 217 | Server = null; 218 | GhostNetWatchdog.StopWatchdog(); 219 | } 220 | } 221 | 222 | public void RunDedicated() { 223 | Logger.Log("ghostnet-s", "GhostNet headless server is online."); 224 | Logger.Log("ghostnet-s", $"Make sure to forward the ports {Settings.Port} TCP and UDP"); 225 | Logger.Log("ghostnet-s", "and to let your firewall allow incoming connections."); 226 | Console.WriteLine(""); 227 | Client.OnHandle += (con, frame) => { 228 | if (frame.Get() != null) 229 | Logger.Log("ghostnet-chat", new GhostNetClient.ChatLine(frame).ToString()); 230 | }; 231 | while (Server.IsRunning) { 232 | string line = Console.ReadLine(); 233 | if (string.IsNullOrWhiteSpace(line)) 234 | continue; 235 | line = line.TrimEnd(); 236 | if (line == "/quit") { 237 | Stop(); 238 | return; 239 | } 240 | Client.SendMChat(line); 241 | } 242 | } 243 | 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetModuleSettings.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod.UI; 2 | using FMOD.Studio; 3 | using Microsoft.Xna.Framework; 4 | using Microsoft.Xna.Framework.Input; 5 | using Monocle; 6 | using System; 7 | using System.Collections; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using YamlDotNet.Serialization; 13 | 14 | namespace Celeste.Mod.Ghost.Net { 15 | public class GhostNetModuleSettings : EverestModuleSettings { 16 | 17 | #region Main Settings 18 | 19 | [SettingInGame(false)] 20 | public string Name { 21 | get { 22 | return GhostModule.Settings.Name; 23 | } 24 | set { 25 | GhostModule.Settings.Name = value; 26 | } 27 | } 28 | 29 | [YamlIgnore] 30 | public bool Connection { 31 | get { 32 | return GhostNetModule.Instance.Client?.Connection != null; 33 | } 34 | set { 35 | if (value) { 36 | GhostNetModule.ResetGhostModuleSettings(); 37 | 38 | GhostNetModule.Instance.Start(); 39 | } else { 40 | GhostNetModule.Instance.Stop(); 41 | } 42 | if (ServerEntry != null) 43 | ServerEntry.Disabled = value; 44 | } 45 | } 46 | [YamlIgnore] 47 | [SettingIgnore] 48 | public TextMenu.OnOff EnabledEntry { get; protected set; } 49 | 50 | [SettingIgnore] 51 | [YamlMember(Alias = "Server")] 52 | public string _Server { get; set; } = "celeste.0x0ade.ga"; 53 | [YamlIgnore] 54 | public string Server { 55 | get { 56 | return _Server; 57 | } 58 | set { 59 | _Server = value; 60 | 61 | if (Connection) 62 | GhostNetModule.Instance.Start(); 63 | } 64 | } 65 | [YamlIgnore] 66 | [SettingIgnore] 67 | public TextMenu.Button ServerEntry { get; protected set; } 68 | 69 | #endregion 70 | 71 | #region Client Settings 72 | 73 | public bool Collision { get; set; } = true; 74 | 75 | public bool Sounds { get; set; } = true; 76 | 77 | [SettingIgnore] 78 | // [SettingRange(0, 3)] 79 | public int SendFrameSkip { get; set; } = 0; 80 | 81 | [SettingIgnore] 82 | [SettingRange(4, 16)] 83 | public int ChatLogLength { get; set; } = 8; 84 | 85 | [SettingIgnore] 86 | public bool SendUFramesInMStream { get; set; } = false; 87 | 88 | [SettingIgnore] 89 | public string[] EmoteFavs { get; set; } 90 | 91 | #endregion 92 | 93 | #region Server Settings 94 | [SettingIgnore] 95 | public string ServerName { get; set; } = ""; 96 | [SettingIgnore] 97 | [YamlIgnore] 98 | public string ServerNameAuto => 99 | !string.IsNullOrEmpty(ServerName) ? ServerName : 100 | $"{GhostModule.Settings.Name}'{(GhostModule.Settings.Name.ToLowerInvariant().EndsWith("s") ? "" : "s")} server"; 101 | 102 | [SettingIgnore] 103 | public string ServerMessageGreeting { get; set; } = "Welcome ((player))#((id)), to ((server))!"; 104 | [SettingIgnore] 105 | public string ServerMessageMOTD { get; set; } = 106 | @"Don't cheat and have fun! 107 | Press T to talk. 108 | Send /help for a list of all commands."; 109 | [SettingIgnore] 110 | public string ServerMessageLeave { get; set; } = "Cya, ((player))#((id))!"; 111 | 112 | [SettingIgnore] 113 | public int ServerMaxNameLength { get; set; } = 16; 114 | [SettingIgnore] 115 | public int ServerMaxEmoteValueLength { get; set; } = 2048; 116 | [SettingIgnore] 117 | public int ServerMaxChatTextLength { get; set; } = 256; 118 | [SettingIgnore] 119 | public string ServerCommandPrefix { get; set; } = "/"; 120 | 121 | [YamlMember(Alias = "ServerColorInfo")] 122 | [SettingIgnore] 123 | public string ServerColorDefaultHex { 124 | get { 125 | return ServerColorDefault.R.ToString("X2") + ServerColorDefault.G.ToString("X2") + ServerColorDefault.B.ToString("X2"); 126 | } 127 | set { 128 | if (string.IsNullOrEmpty(value)) 129 | return; 130 | try { 131 | ServerColorDefault = Calc.HexToColor(value); 132 | } catch (Exception e) { 133 | Logger.Log(LogLevel.Warn, "rainbowmod", "Invalid ServerColorDefault!"); 134 | e.LogDetailed(); 135 | } 136 | } 137 | } 138 | [YamlIgnore] 139 | [SettingIgnore] 140 | public Color ServerColorDefault { get; set; } = Color.LightSlateGray; 141 | 142 | [YamlMember(Alias = "ServerColorBroadcast")] 143 | [SettingIgnore] 144 | public string ServerColorBroadcastHex { 145 | get { 146 | return ServerColorBroadcast.R.ToString("X2") + ServerColorBroadcast.G.ToString("X2") + ServerColorBroadcast.B.ToString("X2"); 147 | } 148 | set { 149 | if (string.IsNullOrEmpty(value)) 150 | return; 151 | try { 152 | ServerColorBroadcast = Calc.HexToColor(value); 153 | } catch (Exception e) { 154 | Logger.Log(LogLevel.Warn, "rainbowmod", "Invalid ServerColorBroadcast!"); 155 | e.LogDetailed(); 156 | } 157 | } 158 | } 159 | [YamlIgnore] 160 | [SettingIgnore] 161 | public Color ServerColorBroadcast { get; set; } = Color.Yellow; 162 | 163 | [YamlMember(Alias = "ServerColorError")] 164 | [SettingIgnore] 165 | public string ServerColorErrorHex { 166 | get { 167 | return ServerColorError.R.ToString("X2") + ServerColorError.G.ToString("X2") + ServerColorError.B.ToString("X2"); 168 | } 169 | set { 170 | if (string.IsNullOrEmpty(value)) 171 | return; 172 | try { 173 | ServerColorError = Calc.HexToColor(value); 174 | } catch (Exception e) { 175 | Logger.Log(LogLevel.Warn, "rainbowmod", "Invalid ServerColorError!"); 176 | e.LogDetailed(); 177 | } 178 | } 179 | } 180 | [YamlIgnore] 181 | [SettingIgnore] 182 | public Color ServerColorError { get; set; } = Color.MediumVioletRed; 183 | 184 | [YamlMember(Alias = "ServerColorCommand")] 185 | [SettingIgnore] 186 | public string ServerColorCommandHex { 187 | get { 188 | return ServerColorCommand.R.ToString("X2") + ServerColorCommand.G.ToString("X2") + ServerColorCommand.B.ToString("X2"); 189 | } 190 | set { 191 | if (string.IsNullOrEmpty(value)) 192 | return; 193 | try { 194 | ServerColorCommand = Calc.HexToColor(value); 195 | } catch (Exception e) { 196 | Logger.Log(LogLevel.Warn, "rainbowmod", "Invalid ServerColorCommand!"); 197 | e.LogDetailed(); 198 | } 199 | } 200 | } 201 | [YamlIgnore] 202 | [SettingIgnore] 203 | public Color ServerColorCommand { get; set; } = Color.DarkOliveGreen; 204 | 205 | [YamlMember(Alias = "ServerColorEmote")] 206 | [SettingIgnore] 207 | public string ServerColorEmoteHex { 208 | get { 209 | return ServerColorEmote.R.ToString("X2") + ServerColorEmote.G.ToString("X2") + ServerColorEmote.B.ToString("X2"); 210 | } 211 | set { 212 | if (string.IsNullOrEmpty(value)) 213 | return; 214 | try { 215 | ServerColorEmote = Calc.HexToColor(value); 216 | } catch (Exception e) { 217 | Logger.Log(LogLevel.Warn, "rainbowmod", "Invalid ServerColorEmote!"); 218 | e.LogDetailed(); 219 | } 220 | } 221 | } 222 | [YamlIgnore] 223 | [SettingIgnore] 224 | public Color ServerColorEmote { get; set; } = Color.LightSeaGreen; 225 | 226 | public string[] ServerRemoteOpIPs { get; set; } = { "192.168.2.1" }; 227 | 228 | #endregion 229 | 230 | #region Input Settings 231 | 232 | public List ButtonPlayerList { get; set; } = new List(); 233 | public List ButtonEmoteSend { get; set; } = new List(); 234 | public List ButtonChat { get; set; } = new List(); 235 | 236 | #endregion 237 | 238 | #region Helpers 239 | 240 | [SettingIgnore] 241 | [YamlIgnore] 242 | public string Host { 243 | get { 244 | string server = Server.ToLowerInvariant(); 245 | int indexOfPort; 246 | int port; 247 | if (!string.IsNullOrEmpty(Server) && 248 | (indexOfPort = server.LastIndexOf(':')) != -1 && 249 | int.TryParse(server.Substring(indexOfPort + 1), out port) 250 | ) { 251 | return server.Substring(0, indexOfPort); 252 | } 253 | 254 | return server; 255 | } 256 | } 257 | [SettingIgnore] 258 | [YamlIgnore] 259 | public int Port { 260 | get { 261 | string server = Server; 262 | int indexOfPort; 263 | int port; 264 | if (!string.IsNullOrEmpty(Server) && 265 | (indexOfPort = server.LastIndexOf(':')) != -1 && 266 | int.TryParse(server.Substring(indexOfPort + 1), out port) 267 | ) { 268 | return port; 269 | } 270 | 271 | // Default port 272 | return 2782; 273 | } 274 | } 275 | 276 | [SettingIgnore] 277 | [YamlIgnore] 278 | public bool IsHost => 279 | Host == "localhost" || 280 | Host == "127.0.0.1" 281 | ; 282 | 283 | #endregion 284 | 285 | #region Custom Entry Creators 286 | 287 | public void CreateConnectionEntry(TextMenu menu, bool inGame) { 288 | if (Celeste.PlayMode == Celeste.PlayModes.Debug) 289 | menu.Add(new TextMenu.SubHeader("modoptions_ghostnetmodule_debugwarn".DialogCleanOrNull() ?? "WARNING: DEBUG MODE DETECTED!")); 290 | menu.Add( 291 | (EnabledEntry = new TextMenu.OnOff("modoptions_ghostnetmodule_connected".DialogCleanOrNull() ?? "Connected", Connection)) 292 | .Change(v => Connection = v) 293 | ); 294 | } 295 | 296 | public void CreateServerEntry(TextMenu menu, bool inGame) { 297 | menu.Add( 298 | (ServerEntry = new TextMenu.Button(("modoptions_ghostnetmodule_server".DialogCleanOrNull() ?? "Server") + ": " + Server)) 299 | .Pressed(() => { 300 | Audio.Play("event:/ui/main/savefile_rename_start"); 301 | menu.SceneAs().Goto().Init( 302 | Server, 303 | v => Server = v, 304 | maxValueLength: 30 305 | ); 306 | }) 307 | ); 308 | ServerEntry.Disabled = inGame || Connection; 309 | } 310 | 311 | #endregion 312 | 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetParticleHelper.cs: -------------------------------------------------------------------------------- 1 | using Celeste.Mod.Helpers; 2 | using FMOD.Studio; 3 | using Microsoft.Xna.Framework; 4 | using Monocle; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using YamlDotNet.Serialization; 14 | 15 | namespace Celeste.Mod.Ghost.Net { 16 | public static class GhostNetParticleHelper { 17 | 18 | private readonly static List AllTypes = (List) typeof(ParticleType).GetField("AllTypes", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); 19 | 20 | public static int GetID(this ParticleType type) { 21 | return AllTypes.IndexOf(type); 22 | } 23 | 24 | public static ParticleType GetType(int id) { 25 | if (id < 0 || AllTypes.Count <= id) 26 | return null; 27 | return AllTypes[id]; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /GhostNetMod/GhostNetWatchdog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Timers; 4 | using Tmds.Systemd; 5 | 6 | namespace Celeste.Mod.Ghost.Net { 7 | public static class GhostNetWatchdog { 8 | private static Timer watchdogTimer; 9 | 10 | private static bool forceRestart = false; 11 | 12 | public static void ForceRestart() { 13 | forceRestart = true; 14 | } 15 | 16 | public static int DuplicateUsers() { 17 | var names = GhostNetModule.Instance.Server.PlayerMap.Values.Select(e => e.Name); 18 | return names.Count() - names.Distinct().Count(); 19 | } 20 | 21 | private static void Watchdog(object sender, ElapsedEventArgs e) { 22 | if (Environment.GetEnvironmentVariable("WATCHDOG_USEC") == null) return; // prevent error 23 | if (forceRestart) return; // fail if op forces restart 24 | 25 | if (DuplicateUsers() > 2) return; // more than 2 ghost users 26 | 27 | ServiceManager.Notify(ServiceState.Watchdog); 28 | } 29 | 30 | public static void InitializeWatchdog() { 31 | StopWatchdog(); // safety 32 | double microseconds; 33 | if (!Double.TryParse(Environment.GetEnvironmentVariable("WATCHDOG_USEC"), out microseconds)) return; 34 | 35 | double interval = microseconds / 2000; // microseconds / 2 to ms 36 | 37 | watchdogTimer = new Timer(interval); 38 | watchdogTimer.Elapsed += Watchdog; 39 | watchdogTimer.Start(); 40 | } 41 | 42 | public static void StopWatchdog() { 43 | if (watchdogTimer != null) watchdogTimer.Dispose(); 44 | watchdogTimer = null; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /GhostNetMod/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("GhostNetMod")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("GhostNetMod")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("2f04df2a-9512-41a4-b981-2b86a31e4e45")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Everest Team 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 | # Everest GhostMod 2 | 3 | ### License: MIT 4 | 5 | ---- 6 | 7 | Sample mod - Add ghost functionality to Celeste, similar to time trials. 8 | 9 | [![preview](https://i.imgur.com/aXtUTFO.gif)](https://www.youtube.com/watch?v=IQbAItCJQk8) 10 | 11 | [Install Everest](https://everestapi.github.io/), download the mod `.zip` [here](/releases) and place it in your `Mods` directory. Don't extract anything. 12 | 13 | For more information about mod _development_, read the [Everest README.md](https://github.com/EverestAPI/Everest/blob/master/README.md) 14 | -------------------------------------------------------------------------------- /deps/MMHOOK_Celeste.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/deps/MMHOOK_Celeste.dll -------------------------------------------------------------------------------- /deps/System.Buffers.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/deps/System.Buffers.dll -------------------------------------------------------------------------------- /deps/System.Memory.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/deps/System.Memory.dll -------------------------------------------------------------------------------- /deps/System.Runtime.CompilerServices.Unsafe.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/deps/System.Runtime.CompilerServices.Unsafe.dll -------------------------------------------------------------------------------- /deps/Tmds.Systemd.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EverestAPI/GhostMod/b89698481686d46dea5806631f14136b300204bc/deps/Tmds.Systemd.dll -------------------------------------------------------------------------------- /deps/Tmds.Systemd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tmds.Systemd 5 | 6 | 7 | 8 | 9 | Interact with the journal service. 10 | 11 | 12 | 13 | Returns whether the journal service is currently available. 14 | 15 | 16 | Returns whether the journal service can be available. 17 | 18 | 19 | The syslog identifier added to each log message. 20 | 21 | 22 | Obtain a cleared JournalMessage. The Message must be Disposed to return it. 23 | 24 | 25 | 26 | Submit a log entry to the journal. 27 | 28 | 29 | 30 | 31 | Represents a valid journal field name. 32 | 33 | 34 | 35 | Priority value. 36 | 37 | 38 | Syslog identifier tag. 39 | 40 | 41 | Human readable message. 42 | 43 | 44 | Constructor 45 | 46 | 47 | Length of the name. 48 | 49 | 50 | Conversion to ReadOnlySpan. 51 | 52 | 53 | Conversion from string. 54 | 55 | 56 | Returns the string representation of this name. 57 | 58 | 59 | Conversion to string. 60 | 61 | 62 | Checks equality. 63 | 64 | 65 | Equality comparison. 66 | 67 | 68 | Inequality comparison. 69 | 70 | 71 | Checks equality. 72 | 73 | 74 | Returns the hash code for this name. 75 | 76 | 77 | Represents a structured log message. 78 | 79 | 80 | Destructor. 81 | 82 | 83 | Appends a field to the message. 84 | 85 | 86 | Appends a field to the message. 87 | 88 | 89 | Appends a field to the message. 90 | 91 | 92 | Appends a field to the message. 93 | 94 | 95 | Appends a field to the message. 96 | 97 | 98 | Appends a field to the message. 99 | 100 | 101 | Returns the JournalMessage. 102 | 103 | 104 | 105 | Log flags. 106 | 107 | 108 | 109 | No flags. 110 | 111 | 112 | System is unusable. 113 | 114 | 115 | Action must be taken immediately. 116 | 117 | 118 | Critical conditions. 119 | 120 | 121 | Error conditions. 122 | 123 | 124 | Warning conditions. 125 | 126 | 127 | Normal but significant conditions. 128 | 129 | 130 | Informational. 131 | 132 | 133 | Debug-level messages. 134 | 135 | 136 | Drop the message instead of blocking. 137 | 138 | 139 | Don't append a syslog identifier. 140 | 141 | 142 | Result of a log operation. 143 | 144 | 145 | Message sent succesfully. 146 | 147 | 148 | Unknown error. 149 | 150 | 151 | Logging service is not available. 152 | 153 | 154 | Logging service is not supported. 155 | 156 | 157 | Message is too large to be sent. 158 | 159 | 160 | Logging would block. 161 | 162 | 163 | 164 | Interact with the systemd system manager. 165 | 166 | 167 | 168 | 169 | Returns whether the process is running as part of a unit. 170 | 171 | 172 | 173 | 174 | Returns unique identifier of the runtime cycle of the unit. 175 | 176 | 177 | 178 | 179 | Notify service manager about start-up completion and other service status changes. 180 | 181 | 182 | 183 | 184 | Instantiate Sockets for the file descriptors passed by the service manager. 185 | 186 | 187 | 188 | 189 | Describes a service state change. 190 | 191 | 192 | 193 | 194 | Service startup is finished. 195 | 196 | 197 | 198 | 199 | Service is reloading its configuration. 200 | 201 | 202 | 203 | 204 | Service is beginning its shutdown. 205 | 206 | 207 | 208 | 209 | Update the watchdog timestamp. 210 | 211 | 212 | 213 | 214 | Describes the service state. 215 | 216 | 217 | 218 | 219 | Describes the service failure (errno-style). 220 | 221 | 222 | 223 | 224 | Describes the service failure (D-Bus error). 225 | 226 | 227 | 228 | 229 | Main process ID (PID) of the service, in case the service manager did not fork off the process itself. 230 | 231 | 232 | 233 | 234 | Create custom ServiceState. 235 | 236 | 237 | 238 | 239 | String representation of service state. 240 | 241 | 242 | 243 | Represents a Unix Domain Socket endpoint as a path. 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /everest.yaml: -------------------------------------------------------------------------------- 1 | - Name: GhostMod 2 | Version: 1.2.1 3 | DLL: GhostMod.dll 4 | Dependencies: 5 | - Name: Everest 6 | Version: 1.0.0 7 | 8 | - Name: GhostNetMod 9 | Version: 1.3.16 10 | DLL: GhostNetMod.dll 11 | Dependencies: 12 | - Name: Everest 13 | Version: 1.0.0 14 | - Name: GhostMod 15 | Version: 1.2.1 16 | --------------------------------------------------------------------------------