├── .editorconfig ├── .gitattributes ├── .gitattributes.txt ├── .gitignore ├── API ├── Generator.cs ├── Legacy │ ├── LegacyGenerator.cs │ └── LegacySaver.cs ├── MultiStructureGenerator.cs └── Saver.cs ├── AssGenConfig.txt ├── Assets ├── GUI │ ├── Box.png │ ├── Cross.png │ ├── Down.png │ ├── DownLarge.png │ ├── Eye.png │ ├── EyeClosed.png │ ├── Gradient.png │ ├── Null.png │ ├── PlusB.png │ ├── PlusG.png │ ├── PlusP.png │ ├── PlusR.png │ ├── Refresh.png │ ├── Up.png │ └── UpLarge.png ├── Items │ ├── ChestWand.png │ ├── MultistructureWand.png │ ├── PortingWand.png │ ├── StructureWand.png │ └── TestWand.png ├── Tiles │ ├── NullBlock.png │ ├── NullBlockItem.png │ ├── NullTileAndWallPlacer.png │ ├── NullWall.png │ └── NullWallItem.png ├── box.png └── corner.png ├── ChestHelper ├── ChestEntity.cs ├── ChestRule.cs ├── ChestRuleChance.cs ├── ChestRuleGuaranteed.cs ├── ChestRulePool.cs └── ChestRulePoolChance.cs ├── Content ├── GUI │ ├── BoolEditor.cs │ ├── ChestGUI │ │ ├── ChestCustomizerState.cs │ │ ├── ChestRuleElement.cs │ │ ├── LootElement.cs │ │ ├── NumberSetter.cs │ │ └── SpecificChestRuleElements.cs │ ├── FieldEditor.cs │ ├── ManualGeneratorMenu.cs │ ├── NameConfirmPopup.cs │ ├── TextField.cs │ ├── Tooltip.cs │ └── UIRenderer.cs ├── Items │ ├── ChestWand.cs │ ├── MultistructureWand.cs │ ├── PortingWand.cs │ ├── StructureWand.cs │ └── TestWand.cs └── Tiles │ └── NullBlock.cs ├── Core └── Loaders │ └── UILoading │ ├── SmartUIElement.cs │ ├── SmartUIState.cs │ └── UILoader.cs ├── Generator.cs ├── Helper.cs ├── Helpers ├── ErrorHelper.cs ├── GUIHelper.cs └── LocalizationHelper.cs ├── Localization └── en-US.hjson ├── LocalizationRoundabout.cs ├── Models ├── GenFlags.cs ├── Legacy │ └── TileSaveData.cs ├── MultiStructureData.cs ├── NbtEntries │ ├── SignNBTEntry.cs │ └── TileEntityNBTEntry.cs ├── StructureData.cs ├── StructureNbtEntry.cs └── TileDataEntry.cs ├── Porting └── Porter.cs ├── Properties └── launchSettings.json ├── SampleChest ├── StructureHelper.cs ├── StructureHelper.csproj ├── StructureHelper.sln ├── Util ├── LegacyStructurePreview.cs └── StructurePreview.cs ├── WikiPics ├── Buttons.png ├── Weight.png └── noWeight.png ├── build.txt ├── description.txt ├── description_workshop.txt ├── icon.png ├── icon_workshop.png └── workshop.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitattributes.txt: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | # we don't want to remove this *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd 363 | 364 | # Custom 365 | *.idx 366 | FETCH_HEAD 367 | *.pack 368 | refs/remotes/origin/ArmorEnchantAndBags 369 | /Localization/en-US.hjson 370 | -------------------------------------------------------------------------------- /API/Generator.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Helpers; 2 | using StructureHelper.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.IO.Compression; 7 | using Terraria.DataStructures; 8 | using Terraria.ID; 9 | 10 | namespace StructureHelper.API 11 | { 12 | /// 13 | /// In this class you will find various utilities related to generating structures and getting important 14 | /// information from them such as their dimensions. 15 | /// 16 | public class Generator 17 | { 18 | internal static readonly Dictionary StructureCache = []; 19 | 20 | /// 21 | /// Helper to check bounds on a generation call. You can also use this to check the bounds 22 | /// of your own structure 23 | /// 24 | /// The StructureData to check 25 | /// The position to check from, this would be the top-left of the structure. 26 | /// If the structure is in bounds or not 27 | public static bool IsInBounds(StructureData data, Point16 pos) 28 | { 29 | if (pos.X < 0 || pos.X + data.width >= Main.maxTilesX || pos.Y < 0 || pos.Y + data.height >= Main.maxTilesY) 30 | return false; 31 | 32 | return true; 33 | } 34 | 35 | /// 36 | /// Helper to check bounds on a generation call. You can also use this to check the bounds 37 | /// of your own structure 38 | /// 39 | /// The path to search for the structure file 40 | /// The mod to search for the structure file in 41 | /// The position to check from, this would be the top-left of the structure. 42 | /// If the structure is in bounds or not 43 | public static bool IsInBounds(string path, Mod mod, Point16 pos, bool fullPath = false) 44 | { 45 | StructureData data = GetStructureData(path, mod, fullPath); 46 | 47 | if (pos.X < 0 || pos.X + data.width >= Main.maxTilesX || pos.Y < 0 || pos.Y + data.height >= Main.maxTilesY) 48 | return false; 49 | 50 | return true; 51 | } 52 | 53 | /// 54 | /// Gets the dimensions (width and height) of a structure. 55 | /// 56 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 57 | /// The mod to search for the structure file in 58 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 59 | /// 60 | public static Point16 GetStructureDimensions(string path, Mod mod, bool fullPath = false) 61 | { 62 | StructureData data = GetStructureData(path, mod, fullPath); 63 | 64 | return new Point16(data.width, data.height); 65 | } 66 | 67 | /// 68 | /// Gets the StructureData for a given path in a given mod (or absolute path if fullPath is true). 69 | /// Will attempt to retrieve from the StructureData cache first if possible before doing I/O 70 | /// 71 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 72 | /// The mod to search for the structure file in 73 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 74 | /// The StructureData associated with the desired file 75 | /// 76 | public static StructureData GetStructureData(string path, Mod mod, bool fullPath = false) 77 | { 78 | if (!path.EndsWith(".shstruct")) 79 | path += ".shstruct"; 80 | 81 | string key = Path.Combine(mod.Name, path); 82 | 83 | if (StructureCache.ContainsKey(key)) 84 | return StructureCache[key]; 85 | 86 | if (fullPath && !File.Exists(path)) 87 | throw new FileNotFoundException(ErrorHelper.GenerateErrorMessage($"A file at the path {path} does not exist! (did you mean to not pass true to fullPath? You should pass false if the file is in your mod!)", mod)); 88 | 89 | if (!fullPath && !mod.FileExists(path)) 90 | throw new FileNotFoundException(ErrorHelper.GenerateErrorMessage($"A file at the path {path} in mod {mod.DisplayName}({mod.Name}) does not exist! Did you accidentally include your mods name in the path? (for example, you should not pass 'MyMod/Structures/House', but rather 'Structures/House')", mod)); 91 | 92 | using Stream stream = fullPath ? File.OpenRead(path) : mod.GetFileStream(path); 93 | using var compressionStream = new GZipStream(stream, CompressionMode.Decompress); 94 | using BinaryReader reader = new(compressionStream); 95 | StructureCache.Add(key, StructureData.FromStream(reader)); 96 | return StructureCache[key]; 97 | } 98 | 99 | /// 100 | /// This method generates a structure from a structure file. 101 | /// 102 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 103 | /// The position in the world to place the top-left of the structure, in tile coordinates. 104 | /// The mod to search for the structure file in 105 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 106 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll reference. 107 | /// Allows you to pass flags for special generation behavior. See 108 | public static void GenerateStructure(string path, Point16 pos, Mod mod, bool fullPath = false, bool ignoreNull = false, GenFlags flags = GenFlags.None) 109 | { 110 | StructureData data = GetStructureData(path, mod, fullPath); 111 | 112 | if (!IsInBounds(data, pos)) 113 | throw new ArgumentException(ErrorHelper.GenerateErrorMessage($"Attempted to generate a structure out of bounds! {pos} is not a valid position for the structure at {path}. Mods are responsible for bounds-checking their own structures. You can fetch dimension data using GetDimensions or GetMultistructureDimensions.", mod)); 114 | 115 | GenerateFromData(data, pos, ignoreNull, flags); 116 | } 117 | 118 | /// 119 | /// Directly generates a given StructureData into the world. Use this directly if you have 120 | /// a StructureData object in memory you want to generate from. 121 | /// 122 | /// The StructureData to generate 123 | /// The position in the world to place the top-left of the structure, in tile coordinates. 124 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll reference. 125 | /// Allows you to pass flags for special generation behavior. See 126 | public static void GenerateFromData(StructureData data, Point16 pos, bool ignoreNull = false, GenFlags flags = GenFlags.None) 127 | { 128 | if (!IsInBounds(data, pos)) 129 | throw new ArgumentException(ErrorHelper.GenerateErrorMessage($"Attempted to generate a structure out of bounds! {pos} is not a valid position for the structure. Mods are responsible for bounds-checking their own structures. You can fetch dimension data using GetDimensions or GetMultistructureDimensions.", null)); 130 | 131 | for (int k = 0; k < data.width; k++) 132 | { 133 | if (!data.slowColumns[k] || ignoreNull) 134 | { 135 | data.ExportDataColumn(pos.X + k, pos.Y, k, null); 136 | data.ExportDataColumn(pos.X + k, pos.Y, k, null); 137 | data.ExportDataColumn(pos.X + k, pos.Y, k, null); 138 | data.ExportDataColumn(pos.X + k, pos.Y, k, null); 139 | data.ExportDataColumn(pos.X + k, pos.Y, k, null); 140 | } 141 | else 142 | { 143 | data.ExportDataColumnSlow(pos.X + k, pos.Y, k, null); 144 | data.ExportDataColumnSlow(pos.X + k, pos.Y, k, null); 145 | data.ExportDataColumnSlow(pos.X + k, pos.Y, k, null); 146 | data.ExportDataColumnSlow(pos.X + k, pos.Y, k, null); 147 | data.ExportDataColumnSlow(pos.X + k, pos.Y, k, null); 148 | } 149 | } 150 | 151 | // replace nulls from the placeholder null value to the current null ID 152 | if (ignoreNull) 153 | { 154 | for(int x = 0; x < data.width; x++) 155 | { 156 | for(int y = 0; y < data.height; y++) 157 | { 158 | Tile tile = Main.tile[pos.X + x, pos.Y + y]; 159 | 160 | if (tile.TileType == StructureHelper.NULL_IDENTIFIER) 161 | tile.TileType = StructureHelper.NullTileID; 162 | 163 | if (tile.WallType == StructureHelper.NULL_IDENTIFIER) 164 | tile.WallType = StructureHelper.NullWallID; 165 | } 166 | } 167 | } 168 | 169 | // Handle the NBT data if this structure is marked as having any 170 | if (data.containsNbt) 171 | { 172 | for (int k = 0; k < data.nbtData.Count; k++) 173 | { 174 | StructureNBTEntry thisNbt = data.nbtData[k]; 175 | thisNbt.OnGenerate(pos + new Point16(thisNbt.x, thisNbt.y), ignoreNull, flags); 176 | } 177 | } 178 | 179 | // If we're not in worldgen, we should call frame tile to clean up the edges 180 | // and then sync if we're in multiplayer 181 | if (!WorldGen.generatingWorld) 182 | { 183 | for (int x = 0; x < data.width; x++) 184 | { 185 | WorldGen.TileFrame(pos.X + x, pos.Y); 186 | WorldGen.TileFrame(pos.X + x, pos.Y + data.height); 187 | 188 | WorldGen.SquareWallFrame(pos.X + x, pos.Y); 189 | WorldGen.SquareWallFrame(pos.X + x, pos.Y + data.height); 190 | } 191 | 192 | for (int y = 0; y < data.height; y++) 193 | { 194 | WorldGen.TileFrame(pos.X, pos.Y + y); 195 | WorldGen.TileFrame(pos.X + data.width, pos.Y + y); 196 | 197 | WorldGen.SquareWallFrame(pos.X, pos.Y + y); 198 | WorldGen.SquareWallFrame(pos.X + data.width, pos.Y + y); 199 | } 200 | 201 | if (Main.netMode != NetmodeID.SinglePlayer) 202 | NetMessage.SendTileSquare(-1, pos.X, pos.Y, data.width, data.height); 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /API/Legacy/LegacySaver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Terraria.DataStructures; 4 | using Terraria.ID; 5 | using Terraria.ModLoader.IO; 6 | 7 | namespace StructureHelper.API.Legacy 8 | { 9 | /// 10 | /// A static class providing utilities for saving structures. 11 | /// 12 | public static class LegacySaver 13 | { 14 | /// 15 | /// Saves a given region of the world as a structure file 16 | /// 17 | /// The region of the world to save, in tile coordinates 18 | /// The name of the file to save. Automatically defaults to a file named after the date in the SavedStructures folder 19 | public static void SaveToFile(Rectangle target, string targetPath = null, string name = "unnamed structure") 20 | { 21 | if (name == "") 22 | name = "unnamed structure"; 23 | 24 | string path = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 25 | 26 | if (!Directory.Exists(path)) 27 | Directory.CreateDirectory(path); 28 | 29 | string thisPath = targetPath ?? Path.Combine(path, name); 30 | 31 | int counter = 2; 32 | while (File.Exists(thisPath)) 33 | { 34 | thisPath = targetPath ?? Path.Combine(path, name) + $"({counter})"; 35 | counter++; 36 | } 37 | 38 | Main.NewText("Structure saved as " + thisPath, Color.Yellow); 39 | FileStream stream = File.Create(thisPath); 40 | stream.Close(); 41 | 42 | TagCompound tag = SaveStructure(target); 43 | TagIO.ToFile(tag, thisPath); 44 | } 45 | 46 | /// 47 | /// Saves a given list of TagCompounds together as a multistructure file 48 | /// 49 | /// The tags to save 50 | /// The name of the file to save. Automatically defaults to a file named after the date in the SavedStructures folder 51 | public static void SaveMultistructureToFile(ref List toSave, string targetPath = null, string name = "unnamed multistructure") 52 | { 53 | string path = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 54 | 55 | if (!Directory.Exists(path)) 56 | Directory.CreateDirectory(path); 57 | 58 | string thisPath = targetPath ?? Path.Combine(path, name); 59 | 60 | int counter = 2; 61 | while (File.Exists(thisPath)) 62 | { 63 | thisPath = targetPath ?? Path.Combine(path, name) + $"({counter})"; 64 | counter++; 65 | } 66 | 67 | Main.NewText("Structure saved as " + thisPath, Color.Yellow); 68 | FileStream stream = File.Create(thisPath); 69 | stream.Close(); 70 | 71 | var tag = new TagCompound 72 | { 73 | { "Structures", toSave }, 74 | { "Version", StructureHelper.Instance.Version.ToString() } 75 | }; 76 | 77 | TagIO.ToFile(tag, thisPath); 78 | 79 | toSave.Clear(); 80 | } 81 | 82 | /// 83 | /// Transforms a region of the world into a structure TagCompound. Must be called in an unsafe context! 84 | /// 85 | /// The region to transform 86 | /// The TagCompound that can be saved to a structure file 87 | public unsafe static TagCompound SaveStructure(Rectangle target) 88 | { 89 | StructureHelper instance = StructureHelper.Instance; 90 | 91 | var tag = new TagCompound 92 | { 93 | { "Version", instance?.Version.ToString() ?? "Unknown" }, 94 | { "Width", target.Width }, 95 | { "Height", target.Height } 96 | }; 97 | 98 | var data = new List(); 99 | for (int x = target.X; x <= target.X + target.Width; x++) 100 | { 101 | for (int y = target.Y; y <= target.Y + target.Height; y++) 102 | { 103 | Tile tile = Framing.GetTileSafely(x, y); 104 | string tileName; 105 | string wallName; 106 | string teName; 107 | 108 | if (tile.TileType >= TileID.Count) 109 | tileName = ModContent.GetModTile(tile.TileType).Mod.Name + " " + ModContent.GetModTile(tile.TileType).Name; 110 | else 111 | tileName = tile.TileType.ToString(); 112 | 113 | if (tile.WallType >= WallID.Count) 114 | wallName = ModContent.GetModWall(tile.WallType).Mod.Name + " " + ModContent.GetModWall(tile.WallType).Name; 115 | else 116 | wallName = tile.WallType.ToString(); 117 | 118 | TileEntity teTarget = null; //grabbing TE data 119 | var entityTag = new TagCompound(); 120 | 121 | if (TileEntity.ByPosition.ContainsKey(new Point16(x, y))) 122 | teTarget = TileEntity.ByPosition[new Point16(x, y)]; 123 | 124 | if (teTarget != null) 125 | { 126 | var modTileEntity = teTarget as ModTileEntity; 127 | 128 | if (modTileEntity != null) 129 | teName = modTileEntity.Mod.Name + " " + modTileEntity.Name; 130 | else 131 | { 132 | teName = teTarget.type.ToString(); 133 | } 134 | 135 | teTarget.SaveData(entityTag); 136 | } 137 | else 138 | { 139 | teName = ""; 140 | } 141 | 142 | int wallWireData; 143 | short packedLiquidData; 144 | byte brightInvisibleData; 145 | 146 | fixed (void* ptr = &tile.Get()) 147 | { 148 | int* intPtr = (int*)ptr; 149 | intPtr++; 150 | 151 | wallWireData = *intPtr; 152 | } 153 | 154 | fixed (void* ptr = &tile.Get()) 155 | { 156 | short* shortPtr = (short*)ptr; 157 | 158 | packedLiquidData = *shortPtr; 159 | } 160 | 161 | fixed (void* ptr = &tile.Get()) 162 | { 163 | byte* bytePtr = (byte*)ptr; 164 | 165 | brightInvisibleData = *bytePtr; 166 | } 167 | 168 | data.Add( 169 | new TileSaveData( 170 | tileName, 171 | wallName, 172 | tile.TileFrameX, 173 | tile.TileFrameY, 174 | wallWireData, 175 | packedLiquidData, 176 | brightInvisibleData, 177 | teName, 178 | entityTag 179 | )); 180 | } 181 | } 182 | 183 | tag.Add("TileData", data); 184 | return tag; 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /API/MultiStructureGenerator.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Helpers; 2 | using StructureHelper.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.IO.Compression; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Terraria.DataStructures; 11 | using Terraria.ModLoader; 12 | 13 | namespace StructureHelper.API 14 | { 15 | /// 16 | /// In this class you will find various utilities related to generating multi structures and getting important 17 | /// information from them such as their dimensions or structure count. 18 | /// 19 | public class MultiStructureGenerator 20 | { 21 | internal static readonly Dictionary MultiStructureCache = []; 22 | 23 | /// 24 | /// Helper to check bounds on a generation call. You can also use this to check the bounds 25 | /// of your own structure 26 | /// 27 | /// The MultiStructureData to check 28 | /// The position to check from, this would be the top-left of the structure. 29 | /// The index in the multistructure to check the dimensions of. 30 | /// If the structure is in bounds or not 31 | public static bool IsInBounds(MultiStructureData data, int index, Point16 pos) 32 | { 33 | if (index < 0 || index >= data.count) 34 | throw new IndexOutOfRangeException(ErrorHelper.GenerateErrorMessage($"Index {index} is out of bounds for a multistructure! Max index is {data.count - 1}. You can use GetStructureCount to check the max index if needed.", null)); 35 | 36 | StructureData sData = data.structures[index]; 37 | 38 | if (pos.X < 0 || pos.X + sData.width >= Main.maxTilesX || pos.Y < 0 || pos.Y + sData.height >= Main.maxTilesY) 39 | return false; 40 | 41 | return true; 42 | } 43 | 44 | /// 45 | /// Helper to check bounds on a generation call. You can also use this to check the bounds 46 | /// of your own structure 47 | /// 48 | /// The path to search for the multi structure file 49 | /// The mod to search for the multi structure file in 50 | /// The position to check from, this would be the top-left of the structure. 51 | /// The index in the multistructure to check the dimensions of. 52 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 53 | /// If the structure is in bounds or not 54 | public static bool IsInBounds(string path, Mod mod, int index, Point16 pos, bool fullPath = false) 55 | { 56 | MultiStructureData data = GetMultiStructureData(path, mod, fullPath); 57 | 58 | if (index < 0 || index >= data.count) 59 | throw new IndexOutOfRangeException(ErrorHelper.GenerateErrorMessage($"Index {index} is out of bounds for the multistructure at {path}! Max index is {data.count - 1}. You can use GetStructureCount to check the max index if needed.", mod)); 60 | 61 | StructureData sData = data.structures[index]; 62 | 63 | if (pos.X < 0 || pos.X + sData.width >= Main.maxTilesX || pos.Y < 0 || pos.Y + sData.height >= Main.maxTilesY) 64 | return false; 65 | 66 | return true; 67 | } 68 | 69 | /// 70 | /// Gets the dimensions (width and height) of a structure in a multistructure. 71 | /// 72 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 73 | /// The mod to search for the structure file in 74 | /// The index in the multistructure to check the dimensions of. 75 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 76 | /// 77 | public static Point16 GetStructureDimensions(string path, Mod mod, int index, bool fullPath = false) 78 | { 79 | MultiStructureData data = GetMultiStructureData(path, mod, fullPath); 80 | 81 | if (index < 0 || index >= data.count) 82 | throw new IndexOutOfRangeException(ErrorHelper.GenerateErrorMessage($"Index {index} is out of bounds for the multistructure at {path}! Max index is {data.count - 1}. You can use GetStructureCount to check the max index if needed.", mod)); 83 | 84 | StructureData sData = data.structures[index]; 85 | 86 | return new Point16(sData.width, sData.height); 87 | } 88 | 89 | /// 90 | /// Gets the amount of structures inside of a multi structure file 91 | /// 92 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 93 | /// The mod to search for the structure file in 94 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 95 | /// 96 | public static int GetStructureCount(string path, Mod mod, bool fullPath = false) 97 | { 98 | MultiStructureData data = GetMultiStructureData(path, mod, fullPath); 99 | return data.count; 100 | } 101 | 102 | /// 103 | /// Gets the MultiStructureData for a given path in a given mod (or absolute path if fullPath is true). 104 | /// Will attempt to retrieve from the MultiStructureData cache first if possible before doing I/O 105 | /// 106 | /// The path to search for the multi structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 107 | /// The mod to search for the multi structure file in 108 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 109 | /// The MultiStructureData associated with the desired file 110 | /// 111 | public static MultiStructureData GetMultiStructureData(string path, Mod mod, bool fullPath = false) 112 | { 113 | if (!path.EndsWith(".shmstruct")) 114 | path += ".shmstruct"; 115 | 116 | string key = Path.Combine(mod.Name, path); 117 | 118 | if (MultiStructureCache.ContainsKey(key)) 119 | return MultiStructureCache[key]; 120 | 121 | if (fullPath && !File.Exists(path)) 122 | throw new FileNotFoundException(ErrorHelper.GenerateErrorMessage($"A file at the path {path} does not exist! (did you mean to not pass true to fullPath? You should pass false if the file is in your mod!)", mod)); 123 | 124 | if (!fullPath && !mod.FileExists(path)) 125 | throw new FileNotFoundException(ErrorHelper.GenerateErrorMessage($"A file at the path {path} in mod {mod.DisplayName}({mod.Name}) does not exist! Did you accidentally include your mods name in the path? (for example, you should not pass 'MyMod/Structures/House', but rather 'Structures/House')", mod)); 126 | 127 | using Stream stream = fullPath ? File.OpenRead(path) : mod.GetFileStream(path); 128 | using var compressionStream = new GZipStream(stream, CompressionMode.Decompress); 129 | using BinaryReader reader = new(compressionStream); 130 | MultiStructureCache.Add(key, MultiStructureData.FromStream(reader)); 131 | return MultiStructureCache[key]; 132 | } 133 | 134 | /// 135 | /// This method generates a random structure from a multi structure file. 136 | /// 137 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 138 | /// The position in the world to place the top-left of the structure, in tile coordinates. 139 | /// The mod to search for the structure file in 140 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 141 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll reference. 142 | /// Allows you to pass flags for special generation behavior. See 143 | public static void GenerateMultistructureRandom(string path, Point16 pos, Mod mod, bool fullPath = false, bool ignoreNull = false, GenFlags flags = GenFlags.None) 144 | { 145 | MultiStructureData data = GetMultiStructureData(path, mod, fullPath); 146 | 147 | int index; 148 | 149 | if (WorldGen.generatingWorld) 150 | index = WorldGen.genRand.Next(data.count); 151 | else 152 | index = Main.rand.Next(data.count); 153 | 154 | Generator.GenerateFromData(data.structures[index], pos, ignoreNull, flags); 155 | } 156 | 157 | /// 158 | /// Generates a specific structure from a multi structure file 159 | /// 160 | /// The path to search for the structure file. If it is in a mod, it should not include the mods name (for example, it should be "structures/coolHouse", not "CoolHouseMod/structures/coolHouse") 161 | /// The index of the structure to generate out of the multistructure 162 | /// The position in the world to place the top-left of the structure, in tile coordinates. 163 | /// The mod to search for the structure file in 164 | /// If the search path starts at the root of your file system(true) or the provided mod(false). This should usually be false. 165 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll reference. 166 | /// Allows you to pass flags for special generation behavior. See 167 | /// 168 | public static void GenerateMultistructureSpecific(string path, int index, Point16 pos, Mod mod, bool fullPath = false, bool ignoreNull = false, GenFlags flags = GenFlags.None) 169 | { 170 | MultiStructureData data = GetMultiStructureData(path, mod, fullPath); 171 | 172 | if (index < 0 || index >= data.count) 173 | throw new IndexOutOfRangeException(ErrorHelper.GenerateErrorMessage($"Index {index} is out of bounds for the multistructure at {path}! Max index is {data.count - 1}. You can use GetStructureCount to check the max index if needed.", mod)); 174 | 175 | Generator.GenerateFromData(data.structures[index], pos, ignoreNull, flags); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /API/Saver.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Helpers; 2 | using StructureHelper.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.IO.Compression; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace StructureHelper.API 12 | { 13 | /// 14 | /// In this class you will find utilities for saving regions of the world into structure data, 15 | /// and then saving that data into files if needed. 16 | /// 17 | public class Saver 18 | { 19 | /// 20 | /// Saves a region of the world into a StructureData object. 21 | /// 22 | /// The leftmost point of the region 23 | /// The topmost point of the region 24 | /// The width of the region 25 | /// The height of the region 26 | /// a StructureData object representing that region of the world 27 | /// If any part of the region is outside of the world 28 | public static StructureData SaveToStructureData(int x, int y, int width, int height) 29 | { 30 | if (x < 0 || x + width > Main.maxTilesX || y < 0 || y + height > Main.maxTilesY) 31 | throw new ArgumentException(ErrorHelper.GenerateErrorMessage("Attempted to save structure data for a region outside of the world.", null)); 32 | 33 | return StructureData.FromWorld(x, y, width, height); 34 | } 35 | 36 | /// 37 | /// Saves a given StructureData to a file, the location is by the target path, and with the given name. 38 | /// 39 | /// The StructureData to save 40 | /// The path to the directory save the file into 41 | /// The name of the file, NOT including the extension 42 | public static void SaveToFile(StructureData data, string targetPath = null, string name = "unnamed structure") 43 | { 44 | if (string.IsNullOrEmpty(name)) 45 | name = "unnamed structure"; 46 | 47 | string path = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 48 | 49 | if (!Directory.Exists(path)) 50 | Directory.CreateDirectory(path); 51 | 52 | string thisPath = targetPath ?? Path.Combine(path, name); 53 | 54 | int counter = 2; 55 | while (File.Exists(thisPath + ".shstruct")) 56 | { 57 | thisPath = (targetPath ?? Path.Combine(path, name)) + $"({counter})"; 58 | counter++; 59 | } 60 | 61 | thisPath += ".shstruct"; 62 | 63 | using FileStream fileStream = File.Create(thisPath); 64 | using var compressionStream = new GZipStream(fileStream, CompressionLevel.Optimal); 65 | using var writer = new BinaryWriter(compressionStream); 66 | data.Serialize(writer); 67 | 68 | Main.NewText("Structure saved as " + thisPath, Color.Yellow); 69 | } 70 | 71 | /// 72 | /// Saves a given MultiStructureData to a file, the location is by the target path, and with the given name. 73 | /// 74 | /// The MultiStructureData to save 75 | /// The path to the directory save the file into 76 | /// The name of the file, NOT including the extension 77 | public static void SaveMultistructureToFile(MultiStructureData data, string targetPath = null, string name = "unnamed multistructure") 78 | { 79 | if (string.IsNullOrEmpty(name)) 80 | name = "unnamed structure"; 81 | 82 | string path = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 83 | 84 | if (!Directory.Exists(path)) 85 | Directory.CreateDirectory(path); 86 | 87 | string thisPath = targetPath ?? Path.Combine(path, name); 88 | 89 | int counter = 2; 90 | while (File.Exists(thisPath + ".shmstruct")) 91 | { 92 | thisPath = (targetPath ?? Path.Combine(path, name)) + $"({counter})"; 93 | counter++; 94 | } 95 | 96 | thisPath += ".shmstruct"; 97 | 98 | using FileStream fileStream = File.Create(thisPath); 99 | using var compressionStream = new GZipStream(fileStream, CompressionLevel.Optimal); 100 | using var writer = new BinaryWriter(compressionStream); 101 | data.Serialize(writer); 102 | 103 | Main.NewText("MultiStructure saved as " + thisPath, Color.Yellow); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /AssGenConfig.txt: -------------------------------------------------------------------------------- 1 | AssetRoot:StructureHelper\Assets -------------------------------------------------------------------------------- /Assets/GUI/Box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Box.png -------------------------------------------------------------------------------- /Assets/GUI/Cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Cross.png -------------------------------------------------------------------------------- /Assets/GUI/Down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Down.png -------------------------------------------------------------------------------- /Assets/GUI/DownLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/DownLarge.png -------------------------------------------------------------------------------- /Assets/GUI/Eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Eye.png -------------------------------------------------------------------------------- /Assets/GUI/EyeClosed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/EyeClosed.png -------------------------------------------------------------------------------- /Assets/GUI/Gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Gradient.png -------------------------------------------------------------------------------- /Assets/GUI/Null.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Null.png -------------------------------------------------------------------------------- /Assets/GUI/PlusB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/PlusB.png -------------------------------------------------------------------------------- /Assets/GUI/PlusG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/PlusG.png -------------------------------------------------------------------------------- /Assets/GUI/PlusP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/PlusP.png -------------------------------------------------------------------------------- /Assets/GUI/PlusR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/PlusR.png -------------------------------------------------------------------------------- /Assets/GUI/Refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Refresh.png -------------------------------------------------------------------------------- /Assets/GUI/Up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/Up.png -------------------------------------------------------------------------------- /Assets/GUI/UpLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/GUI/UpLarge.png -------------------------------------------------------------------------------- /Assets/Items/ChestWand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Items/ChestWand.png -------------------------------------------------------------------------------- /Assets/Items/MultistructureWand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Items/MultistructureWand.png -------------------------------------------------------------------------------- /Assets/Items/PortingWand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Items/PortingWand.png -------------------------------------------------------------------------------- /Assets/Items/StructureWand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Items/StructureWand.png -------------------------------------------------------------------------------- /Assets/Items/TestWand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Items/TestWand.png -------------------------------------------------------------------------------- /Assets/Tiles/NullBlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Tiles/NullBlock.png -------------------------------------------------------------------------------- /Assets/Tiles/NullBlockItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Tiles/NullBlockItem.png -------------------------------------------------------------------------------- /Assets/Tiles/NullTileAndWallPlacer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Tiles/NullTileAndWallPlacer.png -------------------------------------------------------------------------------- /Assets/Tiles/NullWall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Tiles/NullWall.png -------------------------------------------------------------------------------- /Assets/Tiles/NullWallItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/Tiles/NullWallItem.png -------------------------------------------------------------------------------- /Assets/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/box.png -------------------------------------------------------------------------------- /Assets/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/Assets/corner.png -------------------------------------------------------------------------------- /ChestHelper/ChestEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Terraria.ID; 5 | using Terraria.ModLoader.IO; 6 | 7 | namespace StructureHelper.ChestHelper 8 | { 9 | class ChestEntity : ModTileEntity 10 | { 11 | public List rules = []; 12 | 13 | public void SaveChestRulesFile() 14 | { 15 | string path = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 16 | 17 | if (!Directory.Exists(path)) 18 | Directory.CreateDirectory(path); 19 | 20 | string thisPath = Path.Combine(path, "SavedChest_" + DateTime.Now.ToString("d-M-y----H-m-s-f")); 21 | Main.NewText("Chest data saved as " + thisPath, Color.Yellow); 22 | FileStream stream = File.Create(thisPath); 23 | stream.Close(); 24 | 25 | TagCompound tag = SaveChestRules(); 26 | TagIO.ToFile(tag, thisPath); 27 | } 28 | 29 | public TagCompound SaveChestRules() 30 | { 31 | var tag = new TagCompound 32 | { 33 | { "Count", rules.Count } 34 | }; 35 | 36 | for (int k = 0; k < rules.Count; k++) 37 | { 38 | tag.Add("Rule" + k, rules[k].Serizlize()); 39 | } 40 | 41 | return tag; 42 | } 43 | 44 | public static List LoadChestRules(TagCompound tag) 45 | { 46 | var rules = new List(); 47 | int count = tag.GetInt("Count"); 48 | 49 | for (int k = 0; k < count; k++) 50 | { 51 | rules.Add(ChestRule.Deserialize(tag.GetCompound("Rule" + k))); 52 | } 53 | 54 | return rules; 55 | } 56 | 57 | public static void SetChest(Chest chest, List rules) 58 | { 59 | int index = 0; 60 | rules.ForEach(n => n.PlaceItems(chest, ref index)); 61 | } 62 | 63 | public override void Update() 64 | { 65 | Dust.NewDustPerfect(Position.ToVector2() * 16 + Vector2.UnitY * 8 + Vector2.One.RotatedByRandom(6.28f) * 6, 111, Vector2.Zero, 0, default, 0.5f); 66 | } 67 | 68 | public override void SaveData(TagCompound tag) 69 | { 70 | tag.Add("Count", rules.Count); 71 | 72 | for (int k = 0; k < rules.Count; k++) 73 | { 74 | tag.Add("Rule" + k, rules[k].Serizlize()); 75 | } 76 | } 77 | 78 | public override void LoadData(TagCompound tag) 79 | { 80 | rules = LoadChestRules(tag); 81 | } 82 | 83 | public override bool IsTileValidForEntity(int i, int j) 84 | { 85 | Tile tile = Framing.GetTileSafely(i, j); 86 | return tile.TileType == TileID.Containers || TileID.Sets.BasicChest[tile.TileType]; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /ChestHelper/ChestRule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Terraria.ModLoader.IO; 3 | 4 | namespace StructureHelper.ChestHelper 5 | { 6 | class ChestRule 7 | { 8 | public List pool = []; 9 | 10 | public virtual bool UsesWeight => false; 11 | 12 | public virtual string Name => "Unknown Rule"; 13 | 14 | public virtual string Tooltip => "Probably a bug! Report me!"; 15 | 16 | public Loot AddItem(Item item) 17 | { 18 | var loot = new Loot(item.Clone(), 1); 19 | pool.Add(loot); 20 | 21 | return loot; 22 | } 23 | 24 | public void RemoveItem(Loot loot) 25 | { 26 | pool.Remove(loot); 27 | } 28 | 29 | public virtual void PlaceItems(Chest chest, ref int nextIndex) { } 30 | 31 | public virtual TagCompound Serizlize() 32 | { 33 | return null; 34 | } 35 | 36 | public static ChestRule Deserialize(TagCompound tag) 37 | { 38 | string str = tag.GetString("Type"); 39 | 40 | ChestRule rule = str switch 41 | { 42 | "Guaranteed" => ChestRuleGuaranteed.Deserialize(tag), 43 | "Chance" => ChestRuleChance.Deserialize(tag), 44 | "Pool" => ChestRulePool.Deserialize(tag), 45 | "PoolChance" => ChestRulePoolChance.Deserialize(tag), 46 | _ => null, 47 | }; 48 | 49 | return rule; 50 | } 51 | 52 | public TagCompound SerializePool() 53 | { 54 | var tag = new TagCompound 55 | { 56 | { "Count", pool.Count } 57 | }; 58 | 59 | for (int k = 0; k < pool.Count; k++) 60 | { 61 | tag.Add("Pool" + k, pool[k].Serialize()); 62 | } 63 | 64 | return tag; 65 | } 66 | 67 | public static List DeserializePool(TagCompound tag) 68 | { 69 | var loot = new List(); 70 | int count = tag.GetInt("Count"); 71 | 72 | for (int k = 0; k < count; k++) 73 | { 74 | loot.Add(Loot.Deserialze(tag.GetCompound("Pool" + k))); 75 | } 76 | 77 | return loot; 78 | } 79 | 80 | public virtual ChestRule Clone() 81 | { 82 | var clone = new ChestRule(); 83 | 84 | for (int k = 0; k < pool.Count; k++) 85 | { 86 | clone.pool.Add(pool[k].Clone()); 87 | } 88 | 89 | return clone; 90 | } 91 | } 92 | 93 | class Loot 94 | { 95 | public Item givenItem; 96 | public int min; 97 | public int max; 98 | public int weight; 99 | 100 | public Loot(Item item, int min, int max = 0, int weight = 1) 101 | { 102 | this.min = min; 103 | this.max = max == 0 ? min : max; 104 | this.weight = weight; 105 | 106 | Item newItem = item.Clone(); 107 | newItem.stack = 1; 108 | givenItem = newItem; 109 | 110 | } 111 | 112 | public Item GetLoot() 113 | { 114 | Item item = givenItem.Clone(); 115 | item.stack = WorldGen.genRand.Next(min, max); 116 | return item; 117 | } 118 | 119 | public TagCompound Serialize() 120 | { 121 | var tag = new TagCompound 122 | { 123 | { "Item", givenItem }, 124 | { "Min", min }, 125 | { "Max", max }, 126 | { "Weight", weight} 127 | }; 128 | return tag; 129 | } 130 | 131 | public static Loot Deserialze(TagCompound tag) 132 | { 133 | return new Loot( 134 | tag.Get("Item"), 135 | tag.GetInt("Min"), 136 | tag.GetInt("Max"), 137 | tag.GetInt("Weight")); 138 | } 139 | 140 | public Loot Clone() 141 | { 142 | return new Loot(givenItem.Clone(), min, max, weight); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /ChestHelper/ChestRuleChance.cs: -------------------------------------------------------------------------------- 1 | using Terraria.ModLoader.IO; 2 | 3 | namespace StructureHelper.ChestHelper 4 | { 5 | class ChestRuleChance : ChestRule 6 | { 7 | /// 8 | /// the chance for any individual item from this pool to generate, from 0 to 1. (0 = 0%, 1 = 100%) If you want to generate X items from the pool, use ChestRulePool instead. If you want multiple different chances, add another rule of this type. 9 | /// 10 | public float chance = 0; 11 | 12 | public override string Name => "Chance Rule"; 13 | 14 | public override string Tooltip => "Attempts to generate all items in the rule, \nwith a configurable chance to generate each.\nItems are attempted in the order they appear here"; 15 | 16 | public override void PlaceItems(Chest chest, ref int nextIndex) 17 | { 18 | if (nextIndex >= 40) 19 | return; 20 | 21 | for (int k = 0; k < pool.Count; k++) 22 | { 23 | if (WorldGen.genRand.NextFloat(1) <= chance) 24 | { 25 | chest.item[nextIndex] = pool[k].GetLoot(); 26 | nextIndex++; 27 | } 28 | } 29 | } 30 | 31 | public override TagCompound Serizlize() 32 | { 33 | var tag = new TagCompound() 34 | { 35 | {"Type", "Chance"}, 36 | {"Chance", chance}, 37 | {"Pool", SerializePool()} 38 | }; 39 | 40 | return tag; 41 | } 42 | 43 | public static ChestRule Deserialize(TagCompound tag) 44 | { 45 | var rule = new ChestRuleChance 46 | { 47 | chance = tag.GetFloat("Chance"), 48 | pool = DeserializePool(tag.GetCompound("Pool")) 49 | }; 50 | 51 | return rule; 52 | } 53 | 54 | public override ChestRule Clone() 55 | { 56 | var clone = new ChestRuleChance(); 57 | 58 | for (int k = 0; k < pool.Count; k++) 59 | { 60 | clone.pool.Add(pool[k].Clone()); 61 | } 62 | 63 | clone.chance = chance; 64 | 65 | return clone; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /ChestHelper/ChestRuleGuaranteed.cs: -------------------------------------------------------------------------------- 1 | using Terraria.ModLoader.IO; 2 | 3 | namespace StructureHelper.ChestHelper 4 | { 5 | class ChestRuleGuaranteed : ChestRule 6 | { 7 | public override string Name => "Guaranteed Rule"; 8 | 9 | public override string Tooltip => "Always generates every item in the rule\nItems are generated in the order they appear here"; 10 | 11 | public override void PlaceItems(Chest chest, ref int nextIndex) 12 | { 13 | if (nextIndex >= 40) 14 | return; 15 | 16 | for (int k = 0; k < pool.Count; k++) 17 | { 18 | if (nextIndex >= 40) 19 | return; 20 | 21 | chest.item[nextIndex] = pool[k].GetLoot(); 22 | nextIndex++; 23 | } 24 | } 25 | 26 | public override TagCompound Serizlize() 27 | { 28 | var tag = new TagCompound() 29 | { 30 | {"Type", "Guaranteed"}, 31 | {"Pool", SerializePool()} 32 | }; 33 | 34 | return tag; 35 | } 36 | 37 | public static ChestRule Deserialize(TagCompound tag) 38 | { 39 | var rule = new ChestRuleGuaranteed 40 | { 41 | pool = DeserializePool(tag.GetCompound("Pool")) 42 | }; 43 | 44 | return rule; 45 | } 46 | 47 | public override ChestRule Clone() 48 | { 49 | var clone = new ChestRuleGuaranteed(); 50 | 51 | for (int k = 0; k < pool.Count; k++) 52 | { 53 | clone.pool.Add(pool[k].Clone()); 54 | } 55 | 56 | return clone; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /ChestHelper/ChestRulePool.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Terraria.ModLoader.IO; 3 | 4 | namespace StructureHelper.ChestHelper 5 | { 6 | class ChestRulePool : ChestRule 7 | { 8 | /// 9 | /// How many items from the pool, picked at random, should be placed in the chest. 10 | /// 11 | public int itemsToGenerate; 12 | 13 | public override bool UsesWeight => true; 14 | 15 | public override string Name => "Pool Rule"; 16 | 17 | public override string Tooltip => "Generates a configurable amount of items \nrandomly selected from the rule.\nCan make use of weight."; 18 | 19 | public override void PlaceItems(Chest chest, ref int nextIndex) 20 | { 21 | if (nextIndex >= 40) 22 | return; 23 | 24 | List toLoot = pool; 25 | 26 | for (int k = 0; k < itemsToGenerate; k++) 27 | { 28 | if (nextIndex >= 40) 29 | return; 30 | 31 | int maxWeight = 1; 32 | 33 | foreach (Loot loot in toLoot) 34 | maxWeight += loot.weight; 35 | 36 | int selection = Main.rand.Next(maxWeight); 37 | int weightTotal = 0; 38 | Loot selectedLoot = null; 39 | 40 | for (int i = 0; i < toLoot.Count; i++) 41 | { 42 | weightTotal += toLoot[i].weight; 43 | 44 | if (selection < weightTotal + 1) 45 | { 46 | selectedLoot = toLoot[i]; 47 | toLoot.Remove(selectedLoot); 48 | break; 49 | } 50 | } 51 | 52 | chest.item[nextIndex] = selectedLoot?.GetLoot(); 53 | nextIndex++; 54 | } 55 | } 56 | 57 | public override TagCompound Serizlize() 58 | { 59 | var tag = new TagCompound() 60 | { 61 | {"Type", "Pool"}, 62 | {"ToGenerate", itemsToGenerate}, 63 | {"Pool", SerializePool()} 64 | }; 65 | 66 | return tag; 67 | } 68 | 69 | public static ChestRule Deserialize(TagCompound tag) 70 | { 71 | var rule = new ChestRulePool 72 | { 73 | itemsToGenerate = tag.GetInt("ToGenerate"), 74 | pool = DeserializePool(tag.GetCompound("Pool")) 75 | }; 76 | 77 | return rule; 78 | } 79 | 80 | public override ChestRule Clone() 81 | { 82 | var clone = new ChestRulePool(); 83 | 84 | for (int k = 0; k < pool.Count; k++) 85 | { 86 | clone.pool.Add(pool[k].Clone()); 87 | } 88 | 89 | clone.itemsToGenerate = itemsToGenerate; 90 | 91 | return clone; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /ChestHelper/ChestRulePoolChance.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Terraria.ModLoader.IO; 3 | 4 | namespace StructureHelper.ChestHelper 5 | { 6 | class ChestRulePoolChance : ChestRule 7 | { 8 | /// 9 | /// How many items from the pool, picked at random, should be placed in the chest. 10 | /// 11 | public int itemsToGenerate; 12 | 13 | /// 14 | /// the chance for this item pool to generate at all. 15 | /// 16 | public float chance; 17 | 18 | public override bool UsesWeight => true; 19 | 20 | public override string Name => "Chance + Pool Rule"; 21 | 22 | public override string Tooltip => "Has a configurable chance to generate a \nconfigurable amount of items randomly \nselected from the rule. \nCan make use of weight."; 23 | 24 | public override void PlaceItems(Chest chest, ref int nextIndex) 25 | { 26 | if (nextIndex >= 40) 27 | return; 28 | 29 | if (WorldGen.genRand.NextFloat() <= chance) 30 | { 31 | List toLoot = pool; 32 | 33 | for (int k = 0; k < itemsToGenerate; k++) 34 | { 35 | if (nextIndex >= 40) 36 | return; 37 | 38 | int maxWeight = 1; 39 | 40 | foreach (Loot loot in toLoot) 41 | maxWeight += loot.weight; 42 | 43 | int selection = Main.rand.Next(maxWeight); 44 | int weightTotal = 0; 45 | Loot selectedLoot = null; 46 | 47 | for (int i = 0; i < toLoot.Count; i++) 48 | { 49 | weightTotal += toLoot[i].weight; 50 | 51 | if (selection < weightTotal + 1) 52 | { 53 | selectedLoot = toLoot[i]; 54 | toLoot.Remove(selectedLoot); 55 | break; 56 | } 57 | } 58 | 59 | chest.item[nextIndex] = selectedLoot?.GetLoot(); 60 | nextIndex++; 61 | } 62 | } 63 | } 64 | 65 | public override TagCompound Serizlize() 66 | { 67 | var tag = new TagCompound() 68 | { 69 | {"Type", "PoolChance"}, 70 | {"Chance", chance}, 71 | {"ToGenerate", itemsToGenerate}, 72 | {"Pool", SerializePool()} 73 | }; 74 | 75 | return tag; 76 | } 77 | 78 | public static ChestRule Deserialize(TagCompound tag) 79 | { 80 | var rule = new ChestRulePoolChance 81 | { 82 | itemsToGenerate = tag.GetInt("ToGenerate"), 83 | chance = tag.GetFloat("Chance"), 84 | pool = DeserializePool(tag.GetCompound("Pool")) 85 | }; 86 | 87 | return rule; 88 | } 89 | 90 | public override ChestRule Clone() 91 | { 92 | var clone = new ChestRulePoolChance(); 93 | 94 | for (int k = 0; k < pool.Count; k++) 95 | clone.pool.Add(pool[k].Clone()); 96 | 97 | clone.itemsToGenerate = itemsToGenerate; 98 | clone.chance = chance; 99 | 100 | return clone; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Content/GUI/BoolEditor.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Helpers; 2 | using System; 3 | using Terraria.UI; 4 | 5 | namespace StructureHelper.Content.GUI 6 | { 7 | internal class BoolEditor : FieldEditor 8 | { 9 | public BoolEditor(string name, Action onValueChanged, bool initialValue, Func listenForUpdate = null, string description = "") : base(70, name, onValueChanged, listenForUpdate, initialValue, description) { } 10 | 11 | public override void SafeClick(UIMouseEvent evt) 12 | { 13 | value = !value; 14 | onValueChanged(value); 15 | 16 | Main.isMouseLeftConsumedByUI = true; 17 | } 18 | 19 | public override void SafeDraw(SpriteBatch sprite) 20 | { 21 | Utils.DrawBorderString(sprite, $"{value}", GetDimensions().Position() + new Vector2(12, 38), Color.White, 0.8f); 22 | 23 | var box = GetDimensions().ToRectangle(); 24 | box.Width = 40; 25 | box.Height = 15; 26 | box.Offset(new Point(95, 40)); 27 | GUIHelper.DrawBox(sprite, box); 28 | 29 | if (value) 30 | { 31 | box.Width = 15; 32 | box.Offset(new Point(25, 0)); 33 | GUIHelper.DrawBox(sprite, box, Color.Yellow); 34 | } 35 | else 36 | { 37 | box.Width = 15; 38 | GUIHelper.DrawBox(sprite, box); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Content/GUI/ChestGUI/ChestCustomizerState.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using StructureHelper.Core.Loaders.UILoading; 3 | using System.Collections.Generic; 4 | using Terraria.GameContent.UI.Elements; 5 | using Terraria.UI; 6 | 7 | namespace StructureHelper.Content.GUI.ChestGUI 8 | { 9 | class ChestCustomizerState : SmartUIState 10 | { 11 | internal UIList ruleElements = []; 12 | internal UIScrollbar scrollBar = new(); 13 | 14 | readonly UIImageButton NewGuaranteed = new(Assets.GUI.PlusR); 15 | readonly UIImageButton NewChance = new(Assets.GUI.PlusG); 16 | readonly UIImageButton NewPool = new(Assets.GUI.PlusP); 17 | readonly UIImageButton NewPoolChance = new(Assets.GUI.PlusB); 18 | 19 | public static UIImageButton closeButton = new(Assets.GUI.Cross); 20 | 21 | public override int InsertionIndex(List layers) 22 | { 23 | return layers.FindIndex(layer => layer.Name.Equals("Vanilla: Mouse Text")); 24 | } 25 | 26 | public override void OnInitialize() 27 | { 28 | ManualGeneratorMenu.SetDims(ruleElements, -200, 0.5f, 0, 0.1f, 400, 0, 0, 0.8f); 29 | ManualGeneratorMenu.SetDims(scrollBar, 232, 0.5f, 0, 0.1f, 32, 0, 0, 0.8f); 30 | ruleElements.SetScrollbar(scrollBar); 31 | ruleElements.ListPadding = 10f; 32 | Append(ruleElements); 33 | Append(scrollBar); 34 | 35 | ManualGeneratorMenu.SetDims(NewGuaranteed, -200, 0.5f, -50, 0.1f, 32, 0, 32, 0); 36 | NewGuaranteed.OnLeftClick += (n, m) => ruleElements.Add(new GuaranteedRuleElement()); 37 | Append(NewGuaranteed); 38 | 39 | ManualGeneratorMenu.SetDims(NewChance, -160, 0.5f, -50, 0.1f, 32, 0, 32, 0); 40 | NewChance.OnLeftClick += (n, m) => ruleElements.Add(new ChanceRuleElement()); 41 | Append(NewChance); 42 | 43 | ManualGeneratorMenu.SetDims(NewPool, -120, 0.5f, -50, 0.1f, 32, 0, 32, 0); 44 | NewPool.OnLeftClick += (n, m) => ruleElements.Add(new PoolRuleElement()); 45 | Append(NewPool); 46 | 47 | ManualGeneratorMenu.SetDims(NewPoolChance, -80, 0.5f, -50, 0.1f, 32, 0, 32, 0); 48 | NewPoolChance.OnLeftClick += (n, m) => ruleElements.Add(new PoolChanceRuleElement()); 49 | Append(NewPoolChance); 50 | 51 | ManualGeneratorMenu.SetDims(closeButton, 200 - 32, 0.5f, -50, 0.1f, 32, 0, 32, 0); 52 | closeButton.OnLeftClick += (n, m) => Visible = false; 53 | Append(closeButton); 54 | } 55 | 56 | public bool SetData(ChestEntity entity) 57 | { 58 | entity.rules.Clear(); 59 | 60 | if (ruleElements.Count == 0) 61 | { 62 | entity.Kill(entity.Position.X, entity.Position.Y); 63 | return false; 64 | } 65 | 66 | for (int k = 0; k < ruleElements.Count; k++) 67 | { 68 | entity.rules.Add((ruleElements._items[k] as ChestRuleElement).rule.Clone()); 69 | } 70 | 71 | return true; 72 | } 73 | 74 | public override void Draw(SpriteBatch spriteBatch) 75 | { 76 | Recalculate(); 77 | 78 | var color = new Color(49, 84, 141); 79 | 80 | Helpers.GUIHelper.DrawBox(spriteBatch, NewGuaranteed.GetDimensions().ToRectangle(), NewGuaranteed.IsMouseHovering ? color : color * 0.8f); 81 | Helpers.GUIHelper.DrawBox(spriteBatch, NewChance.GetDimensions().ToRectangle(), NewChance.IsMouseHovering ? color : color * 0.8f); 82 | Helpers.GUIHelper.DrawBox(spriteBatch, NewPool.GetDimensions().ToRectangle(), NewPool.IsMouseHovering ? color : color * 0.8f); 83 | Helpers.GUIHelper.DrawBox(spriteBatch, NewPoolChance.GetDimensions().ToRectangle(), NewPoolChance.IsMouseHovering ? color : color * 0.8f); 84 | 85 | Helpers.GUIHelper.DrawBox(spriteBatch, closeButton.GetDimensions().ToRectangle(), closeButton.IsMouseHovering ? color : color * 0.8f); 86 | 87 | var rect = ruleElements.GetDimensions().ToRectangle(); 88 | rect.Inflate(30, 10); 89 | Helpers.GUIHelper.DrawBox(spriteBatch, rect, new Color(20, 40, 60) * 0.8f); 90 | 91 | if (rect.Contains(Main.MouseScreen.ToPoint())) 92 | Main.LocalPlayer.mouseInterface = true; 93 | 94 | if (NewGuaranteed.IsMouseHovering) 95 | { 96 | Tooltip.SetName("New 'Guaranteed' rule"); 97 | Tooltip.SetTooltip("Guaranteed rules will always place every item in them in this chest, in the order they appear in the rule."); 98 | } 99 | 100 | if (NewChance.IsMouseHovering) 101 | { 102 | Tooltip.SetName("New 'Chance' rule"); 103 | Tooltip.SetTooltip("Chance rules give all items in the rule a chance to generate. You can customize the value for this chance from 0% to 100%"); 104 | } 105 | 106 | if (NewPool.IsMouseHovering) 107 | { 108 | Tooltip.SetName("New 'Pool' rule"); 109 | Tooltip.SetTooltip("Pool rules will select a customizable amount of items from them and place them in the chest."); 110 | } 111 | 112 | if (NewPoolChance.IsMouseHovering) 113 | { 114 | Tooltip.SetName("New 'Pool + Chance' rule"); 115 | Tooltip.SetTooltip("Pool + Chance rules act as a combination of a chance and pool rule -- they have a customizable chance to occur, and if they do, act as a pool rule."); 116 | } 117 | 118 | if (closeButton.IsMouseHovering) 119 | { 120 | Tooltip.SetName("Close"); 121 | Tooltip.SetTooltip("Close this menu"); 122 | } 123 | 124 | base.Draw(spriteBatch); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /Content/GUI/ChestGUI/ChestRuleElement.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using Terraria.GameContent.UI.Elements; 3 | using Terraria.UI; 4 | 5 | namespace StructureHelper.Content.GUI.ChestGUI 6 | { 7 | class ChestRuleElement : UIElement 8 | { 9 | internal ChestRule rule; 10 | internal Color color = Color.White; 11 | internal float storedHeight = 0; 12 | 13 | internal UIList lootElements = []; 14 | readonly UIImageButton removeButton = new(Assets.GUI.Cross); 15 | 16 | readonly UIImageButton upButton = new(Assets.GUI.UpLarge); 17 | readonly UIImageButton downButton = new(Assets.GUI.DownLarge); 18 | readonly UIImageButton hideButton = new(Assets.GUI.Eye); 19 | 20 | public ChestRuleElement(ChestRule rule) 21 | { 22 | this.rule = rule; 23 | 24 | Width.Set(400, 0); 25 | Height.Set(50, 0); 26 | 27 | lootElements.Left.Set(0, 0); 28 | lootElements.Top.Set(50, 0); 29 | lootElements.Width.Set(400, 0); 30 | lootElements.Height.Set(0, 0); 31 | lootElements.ListPadding = 2f; 32 | Append(lootElements); 33 | 34 | removeButton.Left.Set(-36, 1); 35 | removeButton.Top.Set(6, 0); 36 | removeButton.Width.Set(32, 0); 37 | removeButton.Height.Set(32, 0); 38 | removeButton.OnLeftClick += Remove; 39 | Append(removeButton); 40 | 41 | upButton.Left.Set(8, 0); 42 | upButton.Top.Set(-3, 0); 43 | upButton.Width.Set(24, 0); 44 | upButton.Height.Set(18, 0); 45 | upButton.SetVisibility(1, 0.8f); 46 | upButton.OnLeftClick += MoveUp; 47 | Append(upButton); 48 | 49 | downButton.Left.Set(8, 0); 50 | downButton.Top.Set(27, 0); 51 | downButton.Width.Set(24, 0); 52 | downButton.Height.Set(18, 0); 53 | downButton.SetVisibility(1, 0.8f); 54 | downButton.OnLeftClick += MoveDown; 55 | Append(downButton); 56 | 57 | hideButton.Left.Set(-56, 1); 58 | hideButton.Top.Set(16, 0); 59 | hideButton.Width.Set(18, 0); 60 | hideButton.Height.Set(12, 0); 61 | hideButton.SetVisibility(1, 0.5f); 62 | hideButton.OnLeftClick += Hide; 63 | Append(hideButton); 64 | 65 | foreach (Loot loot in rule.pool) 66 | AddItem(loot); 67 | } 68 | 69 | private void Hide(UIMouseEvent evt, UIElement listeningElement) 70 | { 71 | if (storedHeight == 0) 72 | { 73 | hideButton.SetImage(Assets.GUI.EyeClosed); 74 | storedHeight = GetDimensions().Height; 75 | Height.Set(50, 0); 76 | } 77 | else 78 | { 79 | hideButton.SetImage(Assets.GUI.Eye); 80 | Height.Set(storedHeight, 0); 81 | storedHeight = 0; 82 | } 83 | } 84 | 85 | private void MoveDown(UIMouseEvent evt, UIElement listeningElement) 86 | { 87 | var list = Parent.Parent as UIList; 88 | int i = list._items.IndexOf(this); 89 | 90 | if (i < list.Count - 1) 91 | { 92 | (list._items[i + 1], list._items[i]) = (list._items[i], list._items[i + 1]); 93 | } 94 | } 95 | 96 | private void MoveUp(UIMouseEvent evt, UIElement listeningElement) 97 | { 98 | var list = Parent.Parent as UIList; 99 | int i = list._items.IndexOf(this); 100 | 101 | if (i >= 1) 102 | { 103 | (list._items[i - 1], list._items[i]) = (list._items[i], list._items[i - 1]); 104 | } 105 | } 106 | 107 | public override void LeftClick(UIMouseEvent evt) 108 | { 109 | if (Main.mouseItem.IsAir || storedHeight > 0) 110 | return; 111 | 112 | AddItem(Main.mouseItem); 113 | } 114 | 115 | public override void Draw(SpriteBatch spriteBatch) 116 | { 117 | Vector2 pos = GetDimensions().ToRectangle().TopLeft(); 118 | var target = new Rectangle((int)pos.X, (int)pos.Y, (int)GetDimensions().Width, 46); 119 | Helpers.GUIHelper.DrawBox(spriteBatch, target, color); 120 | 121 | if (target.Contains(Main.MouseScreen.ToPoint())) 122 | { 123 | Tooltip.SetName(rule.Name); 124 | Tooltip.SetTooltip("Left click with an item to add to this rule"); 125 | } 126 | 127 | if (removeButton.IsMouseHovering) 128 | { 129 | Tooltip.SetName("Remove rule"); 130 | Tooltip.SetTooltip("Removes this rule."); 131 | } 132 | 133 | if (upButton.IsMouseHovering) 134 | { 135 | Tooltip.SetName("Move up"); 136 | Tooltip.SetTooltip("Moves this rule earlier in priority"); 137 | } 138 | 139 | if (downButton.IsMouseHovering) 140 | { 141 | Tooltip.SetName("Move down"); 142 | Tooltip.SetTooltip("Moves this rule later in priority"); 143 | } 144 | 145 | if (hideButton.IsMouseHovering) 146 | { 147 | Tooltip.SetName("Collapse"); 148 | Tooltip.SetTooltip("Collapse or expand this rule"); 149 | } 150 | 151 | Utils.DrawBorderString(spriteBatch, rule.Name, pos + new Vector2(38, 14), Color.White, 0.9f); 152 | 153 | if (storedHeight == 0) 154 | { 155 | base.Draw(spriteBatch); 156 | } 157 | else 158 | { 159 | removeButton.Draw(spriteBatch); 160 | upButton.Draw(spriteBatch); 161 | downButton.Draw(spriteBatch); 162 | hideButton.Draw(spriteBatch); 163 | } 164 | } 165 | 166 | //These handle adding/removing the elements and items from the appropriate lists, as well as re-sizing the element. 167 | public void AddItem(Item item) 168 | { 169 | Loot loot = rule.AddItem(item); 170 | 171 | var element = new LootElement(loot, rule.UsesWeight); 172 | lootElements.Add(element); 173 | lootElements.Height.Set(lootElements.Height.Pixels + element.Height.Pixels + 2, 0); 174 | Height.Set(Height.Pixels + element.Height.Pixels + 2, 0); 175 | } 176 | 177 | public void AddItem(Loot loot) 178 | { 179 | var element = new LootElement(loot, rule.UsesWeight); 180 | lootElements.Add(element); 181 | lootElements.Height.Set(lootElements.Height.Pixels + element.Height.Pixels + 2, 0); 182 | Height.Set(Height.Pixels + element.Height.Pixels + 2, 0); 183 | } 184 | 185 | public void RemoveItem(Loot loot, LootElement element) 186 | { 187 | rule.RemoveItem(loot); 188 | lootElements.Remove(element); 189 | lootElements.Height.Set(lootElements.Height.Pixels - element.Height.Pixels - 2, 0); 190 | Height.Set(Height.Pixels - element.Height.Pixels - 2, 0); 191 | } 192 | 193 | private void Remove(UIMouseEvent evt, UIElement listeningElement) 194 | { 195 | if (!(Parent.Parent.Parent is ChestCustomizerState)) 196 | return; 197 | 198 | var parent = Parent.Parent.Parent as ChestCustomizerState; 199 | parent.ruleElements.Remove(this); 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /Content/GUI/ChestGUI/LootElement.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using Terraria.GameContent.UI.Elements; 3 | using Terraria.UI; 4 | 5 | namespace StructureHelper.Content.GUI.ChestGUI 6 | { 7 | class LootElement : UIElement 8 | { 9 | readonly Loot loot; 10 | readonly UIImageButton removeButton = new(Assets.GUI.Cross); 11 | 12 | readonly NumberSetter min; 13 | readonly NumberSetter max; 14 | readonly NumberSetter weight; 15 | 16 | readonly UIImageButton upButton = new(Assets.GUI.Up); 17 | readonly UIImageButton downButton = new(Assets.GUI.Down); 18 | 19 | public LootElement(Loot loot, bool hasWeight) 20 | { 21 | this.loot = loot; 22 | 23 | Width.Set(350, 0); 24 | Height.Set(50, 0); 25 | Left.Set(50, 0); 26 | 27 | removeButton.Left.Set(-36, 1); 28 | removeButton.Top.Set(6, 0); 29 | removeButton.Width.Set(32, 0); 30 | removeButton.Height.Set(32, 0); 31 | removeButton.OnLeftClick += Remove; 32 | Append(removeButton); 33 | 34 | min = new NumberSetter(loot.min, "Min", 115); 35 | Append(min); 36 | 37 | max = new NumberSetter(loot.max, "Max", 70); 38 | Append(max); 39 | 40 | if (hasWeight) 41 | { 42 | weight = new NumberSetter(loot.weight, "Weight", 160); 43 | Append(weight); 44 | } 45 | else 46 | { 47 | upButton.Left.Set(8, 0); 48 | upButton.Top.Set(10, 0); 49 | upButton.Width.Set(12, 0); 50 | upButton.Height.Set(8, 0); 51 | upButton.SetVisibility(1, 0.8f); 52 | upButton.OnLeftClick += MoveUp; 53 | Append(upButton); 54 | 55 | downButton.Left.Set(8, 0); 56 | downButton.Top.Set(26, 0); 57 | downButton.Width.Set(12, 0); 58 | downButton.Height.Set(8, 0); 59 | downButton.SetVisibility(1, 0.8f); 60 | downButton.OnLeftClick += MoveDown; 61 | Append(downButton); 62 | } 63 | } 64 | 65 | private void MoveDown(UIMouseEvent evt, UIElement listeningElement) 66 | { 67 | var list = Parent.Parent as UIList; 68 | int i = list._items.IndexOf(this); 69 | 70 | if (i < list.Count - 1) 71 | { 72 | (list._items[i + 1], list._items[i]) = (list._items[i], list._items[i + 1]); 73 | } 74 | } 75 | 76 | private void MoveUp(UIMouseEvent evt, UIElement listeningElement) 77 | { 78 | var list = Parent.Parent as UIList; 79 | int i = list._items.IndexOf(this); 80 | 81 | if (i >= 1) 82 | { 83 | (list._items[i - 1], list._items[i]) = (list._items[i], list._items[i - 1]); 84 | } 85 | } 86 | 87 | public override void Draw(SpriteBatch spriteBatch) 88 | { 89 | Vector2 pos = GetDimensions().ToRectangle().TopLeft(); 90 | var target = new Rectangle((int)pos.X, (int)pos.Y, (int)GetDimensions().Width, 46); 91 | 92 | Color color = Color.White; 93 | 94 | if (Parent.Parent.Parent is ChestRuleElement) 95 | { 96 | color = (Parent.Parent.Parent as ChestRuleElement).color; 97 | color = Color.Lerp(color, Color.Black, 0.25f); 98 | } 99 | 100 | if (removeButton.IsMouseHovering) 101 | { 102 | Tooltip.SetName("Remove item"); 103 | Tooltip.SetTooltip("Remove this item from the rule"); 104 | } 105 | 106 | Helpers.GUIHelper.DrawBox(spriteBatch, target, color); 107 | 108 | int xOff = 0; 109 | if (weight is null) 110 | xOff += 20; 111 | 112 | Main.inventoryScale = 36 / 52f * 46 / 36f; 113 | ItemSlot.Draw(spriteBatch, ref loot.givenItem, 21, pos + Vector2.UnitX * xOff); 114 | 115 | string name = loot.givenItem.Name.Length > 20 ? loot.givenItem.Name[..18] + "..." : loot.givenItem.Name; 116 | Utils.DrawBorderString(spriteBatch, name, pos + new Vector2(46 + xOff, 14), Color.White, 0.8f); 117 | 118 | if (max.Value < min.Value) 119 | max.Value = min.Value; 120 | 121 | loot.min = min.Value; 122 | loot.max = max.Value; 123 | 124 | if (weight != null) 125 | loot.weight = weight.Value; 126 | 127 | base.Draw(spriteBatch); 128 | } 129 | 130 | private void Remove(UIMouseEvent evt, UIElement listeningElement) 131 | { 132 | if (!(Parent.Parent.Parent is ChestRuleElement)) 133 | return; 134 | 135 | var parent = Parent.Parent.Parent as ChestRuleElement; 136 | parent.RemoveItem(loot, this); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /Content/GUI/ChestGUI/NumberSetter.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using Terraria.UI; 3 | 4 | namespace StructureHelper.Content.GUI.ChestGUI 5 | { 6 | class NumberSetter : UIElement 7 | { 8 | readonly string Text; 9 | readonly string Suffix; 10 | 11 | readonly TextField editor = new(InputType.integer); 12 | 13 | public int Value 14 | { 15 | get 16 | { 17 | if (int.TryParse(editor.currentValue, out int result)) 18 | return result; 19 | 20 | return 0; 21 | } 22 | 23 | set => editor.currentValue = value.ToString(); 24 | } 25 | 26 | public NumberSetter(int value, string text, int xOff, string suffix = "") 27 | { 28 | Value = value; 29 | Text = text; 30 | Suffix = suffix; 31 | 32 | Width.Set(32, 0); 33 | Height.Set(50, 0); 34 | Left.Set(-xOff, 1); 35 | 36 | editor.Left.Set(0, 0); 37 | editor.Top.Set(16, 0); 38 | editor.Width.Set(32, 0); 39 | editor.Height.Set(22, 0); 40 | Append(editor); 41 | } 42 | 43 | public override void Draw(SpriteBatch spriteBatch) 44 | { 45 | if (IsMouseHovering) 46 | { 47 | Tooltip.SetName(Text); 48 | Tooltip.SetTooltip("Click to type"); 49 | } 50 | 51 | base.Draw(spriteBatch); 52 | 53 | Utils.DrawBorderString(spriteBatch, Text, GetDimensions().Center() + Vector2.UnitY * -22, Color.White, 0.7f, 0.5f, 0f); 54 | Utils.DrawBorderString(spriteBatch, Suffix, GetDimensions().Center() + new Vector2(16, 6), Color.White, 0.7f, 0.5f, 0.5f); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Content/GUI/ChestGUI/SpecificChestRuleElements.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using System; 3 | 4 | namespace StructureHelper.Content.GUI.ChestGUI 5 | { 6 | class GuaranteedRuleElement : ChestRuleElement 7 | { 8 | public GuaranteedRuleElement() : base(new ChestRuleGuaranteed()) 9 | { 10 | color = new Color(200, 0, 0); 11 | } 12 | 13 | public GuaranteedRuleElement(ChestRule rule) : base(rule) 14 | { 15 | color = new Color(200, 0, 0); 16 | } 17 | } 18 | 19 | class ChanceRuleElement : ChestRuleElement 20 | { 21 | readonly NumberSetter chanceSetter = new(100, "Chance", 100, "%"); 22 | 23 | public ChanceRuleElement() : base(new ChestRuleChance()) 24 | { 25 | color = Color.Green; 26 | Append(chanceSetter); 27 | } 28 | 29 | public ChanceRuleElement(ChestRule rule) : base(rule) 30 | { 31 | color = Color.Green; 32 | chanceSetter.Value = (int)Math.Round((rule as ChestRuleChance).chance * 100); 33 | Append(chanceSetter); 34 | } 35 | 36 | public override void Update(GameTime gameTime) 37 | { 38 | if (chanceSetter.Value > 100) 39 | chanceSetter.Value = 100; 40 | 41 | (rule as ChestRuleChance).chance = chanceSetter.Value / 100f; 42 | base.Update(gameTime); 43 | } 44 | } 45 | 46 | class PoolRuleElement : ChestRuleElement 47 | { 48 | readonly NumberSetter countSetter = new(1, "Amount", 100); 49 | 50 | public PoolRuleElement() : base(new ChestRulePool()) 51 | { 52 | color = Color.Purple; 53 | Append(countSetter); 54 | } 55 | 56 | public PoolRuleElement(ChestRule rule) : base(rule) 57 | { 58 | color = Color.Purple; 59 | countSetter.Value = (rule as ChestRulePool).itemsToGenerate; 60 | Append(countSetter); 61 | } 62 | 63 | public override void Update(GameTime gameTime) 64 | { 65 | if (countSetter.Value > rule.pool.Count) 66 | countSetter.Value = rule.pool.Count; 67 | 68 | (rule as ChestRulePool).itemsToGenerate = countSetter.Value; 69 | base.Update(gameTime); 70 | } 71 | } 72 | 73 | class PoolChanceRuleElement : ChestRuleElement 74 | { 75 | readonly NumberSetter chanceSetter = new(100, "Chance", 100, "%"); 76 | readonly NumberSetter countSetter = new(1, "Amount", 150); 77 | 78 | public PoolChanceRuleElement() : base(new ChestRulePoolChance()) 79 | { 80 | color = new Color(50, 50, 200); 81 | Append(chanceSetter); 82 | Append(countSetter); 83 | } 84 | 85 | public PoolChanceRuleElement(ChestRule rule) : base(rule) 86 | { 87 | color = new Color(50, 50, 200); 88 | chanceSetter.Value = (int)Math.Round((rule as ChestRulePoolChance).chance * 100); 89 | countSetter.Value = (rule as ChestRulePoolChance).itemsToGenerate; 90 | Append(chanceSetter); 91 | Append(countSetter); 92 | } 93 | 94 | public override void Update(GameTime gameTime) 95 | { 96 | if (countSetter.Value > rule.pool.Count) 97 | countSetter.Value = rule.pool.Count; 98 | 99 | if (chanceSetter.Value > 100) 100 | chanceSetter.Value = 100; 101 | 102 | (rule as ChestRulePoolChance).itemsToGenerate = countSetter.Value; 103 | (rule as ChestRulePoolChance).chance = chanceSetter.Value / 100f; 104 | base.Update(gameTime); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /Content/GUI/FieldEditor.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Core.Loaders.UILoading; 2 | using StructureHelper.Helpers; 3 | using System; 4 | 5 | namespace StructureHelper.Content.GUI 6 | { 7 | /// 8 | /// A UI element for changing the value of 'something'. 9 | /// 10 | internal abstract class FieldEditor : SmartUIElement 11 | { 12 | /// 13 | /// The name that gets displated above the panel to the user 14 | /// 15 | public string name; 16 | 17 | /// 18 | /// The info sown when hovering over this panel 19 | /// 20 | public string description; 21 | 22 | /// 23 | /// The current value this editor believes the field its tied to to have. This wont update in real time so be careful 24 | /// 25 | public T value; 26 | 27 | /// 28 | /// The callback that should happen when this editor thinks the value its tracking has changed. You'll likely need to cast the object parameter to the correct type. 29 | /// 30 | protected readonly Action onValueChanged; 31 | 32 | /// 33 | /// This function, called every frame while the editor is not being used, is used to update the editor's value to the current value of the tracked value. 34 | /// 35 | protected readonly Func listenForUpdate; 36 | 37 | /// 38 | /// If this editor is currently being used to change a value, and thus shouldn't listen for update 39 | /// 40 | public virtual bool Editing => false; 41 | 42 | /// 43 | /// 44 | /// 45 | /// Height of the panel 46 | /// The name that gets displated above the panel to the user 47 | /// The callback that should happen when this editor thinks the value its tracking has changed. You'll likely need to cast the object parameter to the correct type. 48 | /// A hint for what the initial value of the field tracked by this editor is 49 | public FieldEditor(int height, string name, Action onValueChanged, Func listenForUpdate = null, T initialValue = default, string description = "") 50 | { 51 | Width.Set(150, 0); 52 | Height.Set(height, 0); 53 | this.name = name; 54 | this.onValueChanged = onValueChanged; 55 | this.listenForUpdate = listenForUpdate; 56 | value = initialValue; 57 | this.description = description; 58 | } 59 | 60 | /// 61 | /// Defines what should happen when a new value is recieved from the value update listener. Note that value has not yet been updated when this is called, so you can compare to the old value. 62 | /// 63 | /// The new value that was recieved 64 | public virtual void OnRecieveNewValue(T newValue) { } 65 | 66 | public sealed override void SafeUpdate(GameTime gameTime) 67 | { 68 | if (!Editing && listenForUpdate != null) 69 | { 70 | T newValue = listenForUpdate(); 71 | OnRecieveNewValue(newValue); 72 | value = newValue; 73 | } 74 | 75 | EditorUpdate(gameTime); 76 | } 77 | 78 | public virtual void EditorUpdate(GameTime gameTime) { } 79 | 80 | public sealed override void Draw(SpriteBatch spriteBatch) 81 | { 82 | GUIHelper.DrawBox(spriteBatch, GetDimensions().ToRectangle()); 83 | 84 | Texture2D back = Assets.GUI.Gradient.Value; 85 | var backTarget = GetDimensions().ToRectangle(); 86 | backTarget.Height = 24; 87 | backTarget.Offset(new Point(4, 4)); 88 | spriteBatch.Draw(back, backTarget, Color.Black * 0.5f); 89 | 90 | Utils.DrawBorderString(spriteBatch, name, GetDimensions().Position() + new Vector2(8, 4), Color.White, 0.7f); 91 | Utils.DrawBorderString(spriteBatch, typeof(T).Name, GetDimensions().Position() + new Vector2(8, 18), Color.Gray, 0.65f); 92 | 93 | base.Draw(spriteBatch); 94 | 95 | SafeDraw(spriteBatch); 96 | 97 | if (IsMouseHovering) 98 | { 99 | Tooltip.SetName(name); 100 | Tooltip.SetTooltip(description); 101 | Main.LocalPlayer.mouseInterface = true; 102 | } 103 | } 104 | 105 | public virtual void SafeDraw(SpriteBatch sprite) { } 106 | } 107 | } -------------------------------------------------------------------------------- /Content/GUI/ManualGeneratorMenu.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.API; 2 | using StructureHelper.Content.Items; 3 | using StructureHelper.Core.Loaders.UILoading; 4 | using StructureHelper.Models; 5 | using StructureHelper.Util; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using Terraria.GameContent.UI.Elements; 10 | using Terraria.ModLoader.UI.Elements; 11 | using Terraria.UI; 12 | 13 | namespace StructureHelper.Content.GUI 14 | { 15 | class ManualGeneratorMenu : SmartUIState 16 | { 17 | public static StructureEntry selected; 18 | public static bool ignoreNulls = false; 19 | 20 | public static StructurePreview preview; 21 | 22 | public static bool multiMode = false; 23 | public static int multiIndex; 24 | 25 | public static UIGrid structureElements = new(); 26 | public static UIScrollbar scrollBar = new(); 27 | 28 | public static UIImageButton refreshButton = new(Assets.GUI.Refresh); 29 | public static UIImageButton ignoreButton = new(Assets.GUI.Null); 30 | public static UIImageButton closeButton = new(Assets.GUI.Cross); 31 | 32 | public static GenFlags flags; 33 | public static BoolEditor slopeNullToggle = new("NullsKeepGivenSlope", (val) => flags = val ? flags | GenFlags.NullsKeepGivenSlope : flags & ~GenFlags.NullsKeepGivenSlope, false, description: "If null tiles will keep their slope when generated"); 34 | public static BoolEditor paintNullToggle = new("NullsKeepGivenPaint", (val) => flags = val ? flags | GenFlags.NullsKeepGivenPaint : flags & ~GenFlags.NullsKeepGivenPaint, false, description: "If null tiles/walls will keep their paint when generated"); 35 | public static BoolEditor tileEntityToggle = new("IgnoreTileEntityData", (val) => flags = val ? flags | GenFlags.IgnoreTileEnttiyData : flags & ~GenFlags.IgnoreTileEnttiyData, false, description: "If custom tile entity data is ignored or not"); 36 | 37 | public override bool Visible => TestWand.UIVisible; 38 | 39 | public override int InsertionIndex(List layers) 40 | { 41 | return layers.FindIndex(layer => layer.Name.Equals("Vanilla: Mouse Text")); 42 | } 43 | 44 | /// 45 | /// Loads all structure files and generates elements to be clicked for them 46 | /// 47 | public static void LoadStructures() 48 | { 49 | structureElements.Clear(); 50 | selected = null; 51 | 52 | string folderPath = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 53 | Directory.CreateDirectory(folderPath); 54 | 55 | string[] filePaths = Directory.GetFiles(folderPath); 56 | 57 | foreach (string path in filePaths) 58 | { 59 | string name = path.Replace(folderPath + Path.DirectorySeparatorChar, ""); 60 | structureElements.Add(new StructureEntry(name, path)); 61 | } 62 | } 63 | 64 | public override void OnInitialize() 65 | { 66 | LoadStructures(); 67 | SetDims(structureElements, -200, 0.5f, 0, 0.1f, 400, 0, 0, 0.8f); 68 | SetDims(scrollBar, 232, 0.5f, 0, 0.1f, 32, 0, 0, 0.8f); 69 | structureElements.SetScrollbar(scrollBar); 70 | Append(structureElements); 71 | Append(scrollBar); 72 | 73 | SetDims(refreshButton, -200, 0.5f, -50, 0.1f, 32, 0, 32, 0); 74 | refreshButton.OnLeftClick += RefreshButton_OnClick; 75 | Append(refreshButton); 76 | 77 | SetDims(ignoreButton, -150, 0.5f, -50, 0.1f, 32, 0, 32, 0); 78 | ignoreButton.OnLeftClick += IgnoreButton_OnClick; 79 | Append(ignoreButton); 80 | 81 | SetDims(closeButton, 200 - 32, 0.5f, -50, 0.1f, 32, 0, 32, 0); 82 | closeButton.OnLeftClick += CloseButton_OnClick; 83 | Append(closeButton); 84 | 85 | SetDims(slopeNullToggle, -384, 0.5f, 0, 0.1f, 150, 0, 70, 0); 86 | Append(slopeNullToggle); 87 | 88 | SetDims(paintNullToggle, -384, 0.5f, 80, 0.1f, 150, 0, 70, 0); 89 | Append(paintNullToggle); 90 | 91 | SetDims(tileEntityToggle, -384, 0.5f, 160, 0.1f, 150, 0, 70, 0); 92 | Append(tileEntityToggle); 93 | } 94 | 95 | private void CloseButton_OnClick(UIMouseEvent evt, UIElement listeningElement) 96 | { 97 | TestWand.UIVisible = false; 98 | Main.isMouseLeftConsumedByUI = true; 99 | } 100 | 101 | private void IgnoreButton_OnClick(UIMouseEvent evt, UIElement listeningElement) 102 | { 103 | ignoreNulls = !ignoreNulls; 104 | Main.isMouseLeftConsumedByUI = true; 105 | } 106 | 107 | private void RefreshButton_OnClick(UIMouseEvent evt, UIElement listeningElement) 108 | { 109 | LoadStructures(); 110 | Main.isMouseLeftConsumedByUI = true; 111 | } 112 | 113 | public override void SafeUpdate(GameTime gameTime) 114 | { 115 | Recalculate(); 116 | 117 | if (Main.playerInventory) 118 | TestWand.UIVisible = false; 119 | 120 | if (ignoreButton.IsMouseHovering) 121 | { 122 | Tooltip.SetName($"Place with null tiles: {ignoreNulls}"); 123 | Tooltip.SetTooltip("If the structure placed manually should have it's null tiles placed or not. Turn this off to get a realistic generation, or on if you want to edit the structure."); 124 | Main.LocalPlayer.mouseInterface = true; 125 | } 126 | 127 | if (refreshButton.IsMouseHovering) 128 | { 129 | Tooltip.SetName("Reload"); 130 | Tooltip.SetTooltip("Reload structures from the folder, use this if you change the folders contents externally and want to see it reflected here."); 131 | Main.LocalPlayer.mouseInterface = true; 132 | } 133 | 134 | if (closeButton.IsMouseHovering) 135 | { 136 | Tooltip.SetName("Close"); 137 | Tooltip.SetTooltip("Close this menu"); 138 | Main.LocalPlayer.mouseInterface = true; 139 | } 140 | } 141 | 142 | public override void Draw(SpriteBatch spriteBatch) 143 | { 144 | var color = new Color(49, 84, 141); 145 | Helpers.GUIHelper.DrawBox(spriteBatch, ignoreButton.GetDimensions().ToRectangle(), ignoreButton.IsMouseHovering ? color : color * 0.8f); 146 | Helpers.GUIHelper.DrawBox(spriteBatch, refreshButton.GetDimensions().ToRectangle(), refreshButton.IsMouseHovering ? color : color * 0.8f); 147 | Helpers.GUIHelper.DrawBox(spriteBatch, closeButton.GetDimensions().ToRectangle(), closeButton.IsMouseHovering ? color : color * 0.8f); 148 | 149 | var rect = structureElements.GetDimensions().ToRectangle(); 150 | rect.Inflate(30, 10); 151 | Helpers.GUIHelper.DrawBox(spriteBatch, rect, new Color(20, 40, 60) * 0.8f); 152 | 153 | base.Draw(spriteBatch); 154 | 155 | if (!ignoreNulls) 156 | { 157 | Texture2D tex = Assets.GUI.Cross.Value; 158 | spriteBatch.Draw(tex, ignoreButton.GetDimensions().ToRectangle(), ignoreButton.IsMouseHovering ? Color.White : Color.White * 0.5f); 159 | } 160 | } 161 | 162 | public static void SetDims(UIElement ele, int x, int y, int w, int h) 163 | { 164 | ele.Left.Set(x, 0); 165 | ele.Top.Set(y, 0); 166 | ele.Width.Set(w, 0); 167 | ele.Height.Set(h, 0); 168 | } 169 | 170 | public static void SetDims(UIElement ele, int x, float xp, int y, float yp, int w, float wp, int h, float hp) 171 | { 172 | ele.Left.Set(x, xp); 173 | ele.Top.Set(y, yp); 174 | ele.Width.Set(w, wp); 175 | ele.Height.Set(h, hp); 176 | } 177 | } 178 | 179 | class StructureEntry : UIElement 180 | { 181 | public string name = ""; 182 | public string path; 183 | 184 | bool Active => ManualGeneratorMenu.selected == this; 185 | 186 | public StructureEntry(string name, string path) 187 | { 188 | this.name = name; 189 | this.path = path; 190 | 191 | Width.Set(400, 0); 192 | Height.Set(32, 0); 193 | } 194 | 195 | public override void Draw(SpriteBatch spriteBatch) 196 | { 197 | if (IsMouseHovering) 198 | Main.LocalPlayer.mouseInterface = true; 199 | 200 | Vector2 pos = GetDimensions().ToRectangle().TopLeft(); 201 | var mainBox = new Rectangle((int)pos.X, (int)pos.Y, 400, 32); 202 | 203 | Color color = Color.Gray; 204 | 205 | if (IsMouseHovering) 206 | color = Color.White; 207 | 208 | if (Active) 209 | color = Color.Yellow; 210 | 211 | Helpers.GUIHelper.DrawBox(spriteBatch, mainBox, IsMouseHovering || Active ? new Color(49, 84, 141) : new Color(49, 84, 141) * 0.6f); 212 | Utils.DrawBorderString(spriteBatch, name, mainBox.Center() + Vector2.UnitY * 4, color, 0.8f, 0.5f, 0.5f); 213 | 214 | base.Draw(spriteBatch); 215 | 216 | if (!Active) 217 | { 218 | Height.Set(32, 0); 219 | RemoveAllChildren(); 220 | } 221 | } 222 | 223 | public override void LeftClick(UIMouseEvent evt) 224 | { 225 | ManualGeneratorMenu.selected = this; 226 | ManualGeneratorMenu.multiIndex = 0; 227 | ManualGeneratorMenu.multiMode = false; 228 | 229 | if (Path.GetExtension(path) == ".shstruct") 230 | { 231 | ManualGeneratorMenu.multiMode = false; 232 | 233 | ManualGeneratorMenu.preview?.Dispose(); 234 | ManualGeneratorMenu.preview = new StructurePreview(name, API.Generator.GetStructureData(path, StructureHelper.Instance, true)); 235 | } 236 | else if (Path.GetExtension(path) == ".shmstruct") 237 | { 238 | ManualGeneratorMenu.multiMode = true; 239 | MultiStructureData data = MultiStructureGenerator.GetMultiStructureData(path, StructureHelper.Instance, true); 240 | 241 | ManualGeneratorMenu.preview?.Dispose(); 242 | ManualGeneratorMenu.preview = new StructurePreview("", data.structures[0]); 243 | 244 | Height.Set(36 + 96 * (int)Math.Ceiling(data.count / 4f), 0); 245 | 246 | var list = new UIGrid(); 247 | 248 | for (int k = 0; k < data.count; k++) 249 | { 250 | list.Add(new MultiSelectionEntry(k, data.structures[k])); 251 | } 252 | 253 | list.Width.Set(400, 0); 254 | list.Height.Set(96 * (int)Math.Ceiling(data.count / 4f), 0); 255 | list.Left.Set(0, 0); 256 | list.Top.Set(36, 0); 257 | Append(list); 258 | } 259 | else 260 | { 261 | Main.NewText("This isnt a structure, or is an unported, old 2.0 format structure! You should port your structures."); 262 | return; 263 | } 264 | } 265 | 266 | public override int CompareTo(object obj) 267 | { 268 | if (obj is StructureEntry other) 269 | return name.CompareTo(other.name); 270 | 271 | return base.CompareTo(obj); 272 | } 273 | } 274 | 275 | class MultiSelectionEntry : UIElement 276 | { 277 | public int value; 278 | private readonly StructureData structure; 279 | private readonly StructurePreview preview; 280 | 281 | bool Active => ManualGeneratorMenu.multiIndex == value; 282 | 283 | public MultiSelectionEntry(int index, StructureData structure) 284 | { 285 | value = index; 286 | Width.Set(96, 0); 287 | Height.Set(96, 0); 288 | 289 | this.structure = structure; 290 | preview = new("", structure); 291 | } 292 | 293 | public override void Draw(SpriteBatch spriteBatch) 294 | { 295 | if (IsMouseHovering) 296 | Main.LocalPlayer.mouseInterface = true; 297 | 298 | var dims = GetDimensions().ToRectangle(); 299 | Vector2 pos = dims.TopLeft() + new Vector2(12, 12); 300 | Color color = Color.Gray; 301 | 302 | if (IsMouseHovering) 303 | color = Color.White; 304 | 305 | if (Active) 306 | color = Color.Yellow; 307 | 308 | Helpers.GUIHelper.DrawBox(spriteBatch, GetDimensions().ToRectangle(), IsMouseHovering || Active ? new Color(49, 84, 141) : new Color(49, 84, 141) * 0.6f); 309 | 310 | dims.Inflate(-8, -8); 311 | 312 | if (preview != null) 313 | { 314 | Texture2D tex = preview.preview; 315 | float scale = 1f; 316 | 317 | if (tex.Width > dims.Width || tex.Height > dims.Height) 318 | scale = tex.Width > tex.Height ? dims.Width / (float)tex.Width : dims.Height / (float)tex.Height; 319 | 320 | spriteBatch.Draw(tex, GetDimensions().Center(), null, color, 0, tex.Size() / 2f, scale, 0, 0); 321 | } 322 | 323 | Utils.DrawBorderString(spriteBatch, value.ToString(), pos + Vector2.UnitY * 4, color, 0.8f, 0.5f, 0.5f); 324 | 325 | base.Draw(spriteBatch); 326 | } 327 | 328 | public override void LeftClick(UIMouseEvent evt) 329 | { 330 | ManualGeneratorMenu.multiIndex = value; 331 | 332 | ManualGeneratorMenu.preview?.Dispose(); 333 | ManualGeneratorMenu.preview = new StructurePreview("", structure); 334 | } 335 | 336 | public override int CompareTo(object obj) 337 | { 338 | if (obj is MultiSelectionEntry other) 339 | return value.CompareTo(other.value); 340 | 341 | return base.CompareTo(obj); 342 | } 343 | } 344 | } -------------------------------------------------------------------------------- /Content/GUI/NameConfirmPopup.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Core.Loaders.UILoading; 2 | using System; 3 | using System.Collections.Generic; 4 | using Terraria.GameContent.UI.Elements; 5 | using Terraria.UI; 6 | 7 | namespace StructureHelper.Content.GUI 8 | { 9 | internal class NameConfirmPopup : SmartUIState 10 | { 11 | public static bool visible; 12 | public static Action onConfirm; 13 | 14 | public TextField nameField = new(); 15 | public UIText confirm = new("Confirm"); 16 | public UIText cancel = new("Cancel"); 17 | 18 | public override bool Visible => visible; 19 | 20 | public override int InsertionIndex(List layers) 21 | { 22 | return layers.FindIndex(layer => layer.Name.Equals("Vanilla: Mouse Text")); 23 | } 24 | 25 | public static void OpenConfirmation(Action onConfirm) 26 | { 27 | visible = true; 28 | NameConfirmPopup.onConfirm = onConfirm; 29 | } 30 | 31 | public override void OnInitialize() 32 | { 33 | confirm.OnMouseOver += (a, b) => confirm.TextColor = Color.Yellow; 34 | confirm.OnMouseOut += (a, b) => confirm.TextColor = Color.White; 35 | confirm.PaddingTop = 6; 36 | confirm.OnLeftClick += (a, b) => 37 | { 38 | if (visible) 39 | { 40 | onConfirm?.Invoke(nameField.currentValue); 41 | nameField.currentValue = ""; 42 | visible = false; 43 | } 44 | }; 45 | 46 | cancel.OnMouseOver += (a, b) => cancel.TextColor = Color.Yellow; 47 | cancel.OnMouseOut += (a, b) => cancel.TextColor = Color.White; 48 | cancel.PaddingTop = 6; 49 | cancel.OnLeftClick += (a, b) => visible = false; 50 | 51 | AddElement(nameField, -120, 0.5f, -46, 0.5f, 240, 0, 28, 0); 52 | AddElement(confirm, -100, 0.5f, 0, 0.5f, 80, 0, 32, 0); 53 | AddElement(cancel, 20, 0.5f, 0, 0.5f, 80, 0, 32, 0); 54 | } 55 | 56 | public override void Draw(SpriteBatch spriteBatch) 57 | { 58 | var color = new Color(49, 84, 141); 59 | var back = new Rectangle(Main.screenWidth / 2 - 150, Main.screenHeight / 2 - 100, 300, 150); 60 | 61 | if (back.Contains(Main.MouseScreen.ToPoint())) 62 | Main.LocalPlayer.mouseInterface = true; 63 | 64 | Helpers.GUIHelper.DrawBox(spriteBatch, back, color); 65 | Utils.DrawBorderString(spriteBatch, "Name your creation:", new Vector2(Main.screenWidth / 2, Main.screenHeight / 2 - 70), Color.White, 1, 0.5f, 0.5f); 66 | 67 | Helpers.GUIHelper.DrawBox(spriteBatch, confirm.GetDimensions().ToRectangle(), color); 68 | Helpers.GUIHelper.DrawBox(spriteBatch, cancel.GetDimensions().ToRectangle(), color); 69 | 70 | base.Draw(spriteBatch); 71 | Recalculate(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Content/GUI/TextField.cs: -------------------------------------------------------------------------------- 1 | using ReLogic.Localization.IME; 2 | using ReLogic.OS; 3 | using StructureHelper.Core.Loaders.UILoading; 4 | using StructureHelper.Helpers; 5 | using System.Text.RegularExpressions; 6 | using Terraria.GameContent; 7 | using Terraria.GameInput; 8 | using Terraria.UI; 9 | 10 | namespace StructureHelper.Content.GUI 11 | { 12 | public enum InputType 13 | { 14 | text, 15 | integer, 16 | number 17 | } 18 | 19 | internal class TextField : SmartUIElement 20 | { 21 | public bool typing; 22 | public bool updated; 23 | public bool reset; 24 | public InputType inputType; 25 | 26 | public string currentValue = ""; 27 | 28 | // Composition string is handled at the very beginning of the update 29 | // In order to check if there is a composition string before backspace is typed, we need to check the previous state 30 | private bool _oldHasCompositionString; 31 | 32 | public TextField(InputType inputType = InputType.text) 33 | { 34 | this.inputType = inputType; 35 | Width.Set(130, 0); 36 | Height.Set(24, 0); 37 | } 38 | 39 | public void SetTyping() 40 | { 41 | typing = true; 42 | Main.blockInput = true; 43 | } 44 | 45 | public void SetNotTyping() 46 | { 47 | typing = false; 48 | Main.blockInput = false; 49 | } 50 | 51 | public override void SafeClick(UIMouseEvent evt) 52 | { 53 | SetTyping(); 54 | } 55 | 56 | public override void SafeRightClick(UIMouseEvent evt) 57 | { 58 | SetTyping(); 59 | currentValue = ""; 60 | updated = true; 61 | } 62 | 63 | public override void SafeUpdate(GameTime gameTime) 64 | { 65 | if (reset) 66 | { 67 | updated = false; 68 | reset = false; 69 | } 70 | 71 | if (updated) 72 | reset = true; 73 | 74 | if (Main.mouseLeft && !IsMouseHovering) 75 | SetNotTyping(); 76 | } 77 | 78 | public void HandleText() 79 | { 80 | if (Main.keyState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape)) 81 | SetNotTyping(); 82 | 83 | PlayerInput.WritingText = true; 84 | Main.instance.HandleIME(); 85 | 86 | string newText = Main.GetInputText(currentValue); 87 | 88 | // GetInputText() handles typing operation, but there is a issue that it doesn't handle backspace correctly when the composition string is not empty. It will delete a character both in the text and the composition string instead of only the one in composition string. We'll fix the issue here to provide a better user experience 89 | if (_oldHasCompositionString && Main.inputText.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Back)) 90 | newText = currentValue; // force text not to be changed 91 | 92 | if (inputType == InputType.integer) 93 | { 94 | if (newText != currentValue && Regex.IsMatch(newText, "^[0-9]*$")) 95 | { 96 | currentValue = newText; 97 | updated = true; 98 | } 99 | } 100 | else if (inputType == InputType.number) //I found this regex on SO so no idea if it works right lol 101 | { 102 | if (newText != currentValue && Regex.IsMatch(newText, "(?<=^| )[0-9]+(.[0-9]+)?(?=$| )|(?<=^| ).[0-9]+(?=$| )")) 103 | { 104 | currentValue = newText; 105 | updated = true; 106 | } 107 | } 108 | else 109 | { 110 | if (newText != currentValue) 111 | { 112 | currentValue = newText; 113 | updated = true; 114 | } 115 | } 116 | 117 | _oldHasCompositionString = Platform.Get().CompositionString is { Length: > 0 }; 118 | } 119 | 120 | public override void Draw(SpriteBatch spriteBatch) 121 | { 122 | GUIHelper.DrawBox(spriteBatch, GetDimensions().ToRectangle(), Color.Black * 0.5f); 123 | 124 | if (typing) 125 | { 126 | GUIHelper.DrawOutline(spriteBatch, GetDimensions().ToRectangle(), Color.White); 127 | HandleText(); 128 | 129 | // draw ime panel, note that if there's no composition string then it won't draw anything 130 | Main.instance.DrawWindowsIMEPanel(GetDimensions().Position()); 131 | } 132 | 133 | Vector2 pos = GetDimensions().Position() + Vector2.One * 4; 134 | 135 | const float scale = 0.75f; 136 | string displayed = currentValue ?? ""; 137 | 138 | Utils.DrawBorderString(spriteBatch, displayed, pos, Color.White, scale); 139 | 140 | // composition string + cursor drawing below 141 | if (!typing) 142 | return; 143 | 144 | pos.X += FontAssets.MouseText.Value.MeasureString(displayed).X * scale; 145 | string compositionString = Platform.Get().CompositionString; 146 | 147 | if (compositionString is { Length: > 0 }) 148 | { 149 | Utils.DrawBorderString(spriteBatch, compositionString, pos, new Color(255, 240, 20), scale); 150 | pos.X += FontAssets.MouseText.Value.MeasureString(compositionString).X * scale; 151 | } 152 | 153 | if (Main.GameUpdateCount % 20 < 10) 154 | Utils.DrawBorderString(spriteBatch, "|", pos, Color.White, scale); 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /Content/GUI/Tooltip.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Core.Loaders.UILoading; 2 | using System; 3 | using System.Collections.Generic; 4 | using Terraria.UI; 5 | using Terraria.UI.Chat; 6 | 7 | namespace StructureHelper.Content.GUI 8 | { 9 | /// 10 | /// Draws the popup tooltip when various elements of the UI are hovered over. 11 | /// 12 | public class Tooltip : SmartUIState, ILoadable 13 | { 14 | private static string text = string.Empty; 15 | private static string tooltip = string.Empty; 16 | 17 | public override bool Visible => true; 18 | 19 | public void Load(Mod mod) 20 | { 21 | On_Main.DrawInterface += Reset; 22 | } 23 | 24 | public override int InsertionIndex(List layers) 25 | { 26 | return layers.Count - 1; 27 | } 28 | 29 | /// 30 | /// Sets the brightly colored main line of the tooltip. This should be a short descriptor of what you're hovering over, like its name 31 | /// 32 | /// 33 | public static void SetName(string name) 34 | { 35 | text = name; 36 | } 37 | 38 | /// 39 | /// Sets the more dimly colored 'description' of the tooltip. This should be the 'body' of the tooltip. 40 | /// 41 | /// 42 | public static void SetTooltip(string newTooltip) 43 | { 44 | ReLogic.Graphics.DynamicSpriteFont font = Terraria.GameContent.FontAssets.MouseText.Value; 45 | tooltip = Helpers.GUIHelper.WrapString(newTooltip, 200, font, 1); 46 | } 47 | 48 | public override void Draw(SpriteBatch spriteBatch) 49 | { 50 | if (text == string.Empty) 51 | return; 52 | 53 | ReLogic.Graphics.DynamicSpriteFont font = Terraria.GameContent.FontAssets.MouseText.Value; 54 | 55 | float nameWidth = ChatManager.GetStringSize(font, text, Vector2.One).X; 56 | float tipWidth = ChatManager.GetStringSize(font, tooltip, Vector2.One).X * 0.9f; 57 | 58 | float width = Math.Max(nameWidth, tipWidth); 59 | float height = -16; 60 | Vector2 pos; 61 | 62 | if (Main.MouseScreen.X > Main.screenWidth - width) 63 | pos = Main.MouseScreen - new Vector2(width + 20, 0); 64 | else 65 | pos = Main.MouseScreen + new Vector2(40, 0); 66 | 67 | height += ChatManager.GetStringSize(font, "{Dummy}\n" + tooltip, Vector2.One).Y + 16; 68 | 69 | if (pos.Y + height > Main.screenHeight) 70 | pos.Y -= height; 71 | 72 | Utils.DrawInvBG(Main.spriteBatch, new Rectangle((int)pos.X - 10, (int)pos.Y - 10, (int)width + 20, (int)height + 20), new Color(20, 20, 55) * 0.925f); 73 | 74 | Utils.DrawBorderString(Main.spriteBatch, text, pos, Color.White); 75 | pos.Y += ChatManager.GetStringSize(font, text, Vector2.One).Y + 4; 76 | 77 | Utils.DrawBorderString(Main.spriteBatch, tooltip, pos, Color.LightGray, 0.9f); 78 | } 79 | 80 | private void Reset(On_Main.orig_DrawInterface orig, Main self, GameTime gameTime) 81 | { 82 | orig(self, gameTime); 83 | 84 | //reset 85 | text = string.Empty; 86 | tooltip = string.Empty; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /Content/GUI/UIRenderer.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Content.Items; 2 | using Terraria.DataStructures; 3 | 4 | namespace StructureHelper.Content.GUI 5 | { 6 | public class UIRenderer : ModSystem 7 | { 8 | public override void Load() 9 | { 10 | On_Main.DrawInterface += DrawSelection; 11 | } 12 | 13 | private void DrawSelection(On_Main.orig_DrawInterface orig, Main self, GameTime gameTime) 14 | { 15 | SpriteBatch spriteBatch = Main.spriteBatch; 16 | 17 | if (Main.LocalPlayer?.HeldItem?.ModItem is StructureWand) 18 | { 19 | var wand = Main.LocalPlayer.HeldItem.ModItem as StructureWand; 20 | 21 | spriteBatch.Begin(default, default, SamplerState.PointClamp, default, default, default, Main.GameViewMatrix.ZoomMatrix); 22 | 23 | Texture2D tex = Assets.corner.Value; 24 | Texture2D tex2 = Assets.box.Value; 25 | 26 | Point16 topLeft = wand.TopLeft; 27 | Point16 bottomRight = wand.BottomRight; 28 | 29 | bool drawPreview = true; 30 | 31 | if (wand.secondPoint) 32 | { 33 | Point16 point1 = wand.point1; 34 | var point2 = (Main.MouseWorld / 16).ToPoint16(); 35 | 36 | topLeft = new Point16(point1.X < point2.X ? point1.X : point2.X, point1.Y < point2.Y ? point1.Y : point2.Y); 37 | bottomRight = new Point16(point1.X > point2.X ? point1.X : point2.X, point1.Y > point2.Y ? point1.Y : point2.Y); 38 | int Width = bottomRight.X - topLeft.X - 1; 39 | int Height = bottomRight.Y - topLeft.Y - 1; 40 | 41 | var target = new Rectangle((int)(topLeft.X * 16 - Main.screenPosition.X), (int)(topLeft.Y * 16 - Main.screenPosition.Y), Width * 16 + 16, Height * 16 + 16); 42 | Helpers.GUIHelper.DrawOutline(spriteBatch, target, Color.Gold); 43 | spriteBatch.Draw(tex2, target, tex2.Frame(), Color.White * 0.15f); 44 | 45 | spriteBatch.Draw(tex, wand.point1.ToVector2() * 16 - Main.screenPosition, tex.Frame(), new Color(50, 200, 255), 0, tex.Frame().Size() / 2, 1, 0, 0); 46 | spriteBatch.Draw(tex, wand.point1.ToVector2() * 16 - Main.screenPosition, tex.Frame(), new Color(155, 155, 50, 0), 0, tex.Frame().Size() / 2, 1, 0, 0); 47 | } 48 | else if (wand.Ready) 49 | { 50 | int Width = bottomRight.X - topLeft.X - 1; 51 | int Height = bottomRight.Y - topLeft.Y - 1; 52 | 53 | var target = new Rectangle((int)(topLeft.X * 16 - Main.screenPosition.X), (int)(topLeft.Y * 16 - Main.screenPosition.Y), Width * 16 + 16, Height * 16 + 16); 54 | Helpers.GUIHelper.DrawOutline(spriteBatch, target, Color.Lerp(Color.Gold, Color.White, 0.5f + 0.5f * (float)System.Math.Sin(Main.GameUpdateCount * 0.2f))); 55 | spriteBatch.Draw(tex2, target, tex2.Frame(), Color.White * 0.15f); 56 | 57 | float scale1 = Vector2.Distance(Main.MouseWorld, wand.point1.ToVector2() * 16) < 32 ? 1.5f : 1f; 58 | spriteBatch.Draw(tex, wand.point1.ToVector2() * 16 - Main.screenPosition, tex.Frame(), new Color(50, 200, 255) * scale1, 0, tex.Frame().Size() / 2, scale1, 0, 0); 59 | spriteBatch.Draw(tex, wand.point1.ToVector2() * 16 - Main.screenPosition, tex.Frame(), new Color(155, 155, 50, 0), 0, tex.Frame().Size() / 2, scale1, 0, 0); 60 | 61 | float scale2 = Vector2.Distance(Main.MouseWorld, wand.point2.ToVector2() * 16) < 32 ? 1.5f : 1f; 62 | spriteBatch.Draw(tex, wand.point2.ToVector2() * 16 - Main.screenPosition, tex.Frame(), new Color(255, 50, 50) * scale2, 0, tex.Frame().Size() / 2, scale2, 0, 0); 63 | spriteBatch.Draw(tex, wand.point2.ToVector2() * 16 - Main.screenPosition, tex.Frame(), new Color(255, 110, 110, 0), 0, tex.Frame().Size() / 2, scale2, 0, 0); 64 | 65 | if (scale1 > 1 || scale2 > 1) 66 | drawPreview = false; 67 | } 68 | 69 | if (drawPreview) 70 | { 71 | var pos = (Main.MouseWorld / 16).ToPoint16(); 72 | spriteBatch.Draw(tex, pos.ToVector2() * 16 - Main.screenPosition, tex.Frame(), Color.White * 0.5f, 0, tex.Frame().Size() / 2, 1, 0, 0); 73 | } 74 | 75 | spriteBatch.End(); 76 | } 77 | 78 | orig(self, gameTime); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /Content/Items/ChestWand.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using StructureHelper.Content.GUI.ChestGUI; 3 | using StructureHelper.Core.Loaders.UILoading; 4 | using Terraria.DataStructures; 5 | using Terraria.ID; 6 | 7 | namespace StructureHelper.Content.Items 8 | { 9 | class ChestWand : ModItem 10 | { 11 | public override string Texture => "StructureHelper/Assets/Items/" + Name; 12 | 13 | public override void SetStaticDefaults() 14 | { 15 | DisplayName.SetDefault("Chest Wand"); 16 | Tooltip.SetDefault("Right click to open the chest rule menu\nLeft click a chest to set the current rules on it\nRight click a chest with rules to copy them"); 17 | } 18 | 19 | public override void SetDefaults() 20 | { 21 | Item.useStyle = ItemUseStyleID.Swing; 22 | Item.useTime = 20; 23 | Item.useAnimation = 20; 24 | Item.rare = ItemRarityID.Blue; 25 | } 26 | 27 | public override bool AltFunctionUse(Player player) 28 | { 29 | return true; 30 | } 31 | 32 | public override bool? UseItem(Player player) 33 | { 34 | Tile tile = Framing.GetTileSafely(Player.tileTargetX, Player.tileTargetY); 35 | 36 | if (tile.TileType == TileID.Containers || TileID.Sets.BasicChest[tile.TileType]) 37 | { 38 | int xOff = tile.TileFrameX % 36 / 18; 39 | int yOff = tile.TileFrameY % 36 / 18; 40 | 41 | if (player.altFunctionUse == 2) 42 | { 43 | if (TileEntity.ByPosition.ContainsKey(new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff))) 44 | { 45 | var chestEntity = TileEntity.ByPosition[new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)] as ChestEntity; 46 | 47 | UILoader.GetUIState().ruleElements.Clear(); 48 | 49 | for (int k = 0; k < chestEntity.rules.Count; k++) 50 | { 51 | ChestRule rule = chestEntity.rules[k].Clone(); 52 | 53 | var elem = new ChestRuleElement(rule); 54 | 55 | if (rule is ChestRuleGuaranteed) 56 | elem = new GuaranteedRuleElement(rule); 57 | 58 | if (rule is ChestRuleChance) 59 | elem = new ChanceRuleElement(rule); 60 | 61 | if (rule is ChestRulePool) 62 | elem = new PoolRuleElement(rule); 63 | 64 | if (rule is ChestRulePoolChance) 65 | elem = new PoolChanceRuleElement(rule); 66 | 67 | UILoader.GetUIState().ruleElements.Add(elem); 68 | } 69 | } 70 | else 71 | { 72 | UILoader.GetUIState().ruleElements.Clear(); 73 | } 74 | 75 | Main.NewText($"Copied chest rules from chest at {new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)}"); 76 | } 77 | else 78 | { 79 | bool overwrite = TileEntity.ByPosition.ContainsKey(new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)); 80 | 81 | TileEntity.PlaceEntityNet(Player.tileTargetX - xOff, Player.tileTargetY - yOff, ModContent.TileEntityType()); 82 | bool cleared = !UILoader.GetUIState().SetData(TileEntity.ByPosition[new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)] as ChestEntity); 83 | 84 | if (overwrite) 85 | { 86 | if (cleared) 87 | Main.NewText($"Removed chest rules for chest at {new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)}", Color.Orange); 88 | else 89 | Main.NewText($"Overwritten chest rules for chest at {new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)}", Color.Yellow); 90 | } 91 | else if (!cleared) 92 | { 93 | Main.NewText($"Set chest rules for chest at {new Point16(Player.tileTargetX - xOff, Player.tileTargetY - yOff)}", Color.GreenYellow); 94 | } 95 | } 96 | } 97 | 98 | if (player.altFunctionUse == 2) 99 | UILoader.GetUIState().Visible = true; 100 | 101 | return true; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Content/Items/MultistructureWand.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.API.Legacy; 2 | using StructureHelper.Content.GUI; 3 | using StructureHelper.Models; 4 | using System.Collections.Generic; 5 | using Terraria.ModLoader.IO; 6 | 7 | namespace StructureHelper.Content.Items 8 | { 9 | class MultistructureWand : StructureWand 10 | { 11 | internal List capturedData = []; 12 | 13 | public override string Texture => "StructureHelper/Assets/Items/" + Name; 14 | 15 | public override void SetStaticDefaults() 16 | { 17 | DisplayName.SetDefault("Multistructure Wand"); 18 | Tooltip.SetDefault("Select 2 points in the world, then right click to add a structure. Right click in your inventory when done to save."); 19 | } 20 | 21 | public override bool CanRightClick() 22 | { 23 | return true; 24 | } 25 | 26 | public override ModItem Clone(Item newEntity) 27 | { 28 | var clone = base.Clone(newEntity) as MultistructureWand; 29 | clone.capturedData = []; 30 | return clone; 31 | } 32 | 33 | public override void RightClick(Player player) 34 | { 35 | Item.stack++; 36 | 37 | if (capturedData.Count > 1) 38 | NameConfirmPopup.OpenConfirmation((name) => API.Saver.SaveMultistructureToFile(MultiStructureData.FromStructureList(capturedData), name: name)); 39 | else 40 | Main.NewText("Not enough structures! If you want to save a single structure, use the normal structure wand instead!", Color.Red); 41 | } 42 | 43 | public override void OnConfirmRectangle() 44 | { 45 | capturedData.Add(API.Saver.SaveToStructureData(TopLeft.X, TopLeft.Y, Width, Height)); 46 | Main.NewText("Structure captured! Total structures to save: " + capturedData.Count); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Content/Items/PortingWand.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.NewFolder; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Terraria.DataStructures; 9 | using Terraria.ID; 10 | 11 | namespace StructureHelper.Content.Items 12 | { 13 | internal class PortingWand : ModItem 14 | { 15 | public override string Texture => "StructureHelper/Assets/Items/" + Name; 16 | 17 | public override void SetStaticDefaults() 18 | { 19 | DisplayName.SetDefault("Porting Wand"); 20 | Tooltip.SetDefault("Automatically ports every structure in your SavedStructures directory."); 21 | } 22 | 23 | public override void SetDefaults() 24 | { 25 | Item.useStyle = ItemUseStyleID.Swing; 26 | Item.useTime = 20; 27 | Item.useAnimation = 20; 28 | Item.rare = ItemRarityID.Blue; 29 | Item.noMelee = true; 30 | Item.noUseGraphic = true; 31 | } 32 | 33 | public override bool? UseItem(Player player) 34 | { 35 | string folderPath = ModLoader.ModPath.Replace("Mods", "SavedStructures"); 36 | Directory.CreateDirectory(folderPath); 37 | 38 | Porter.PortDirectory(folderPath); 39 | return true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Content/Items/StructureWand.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.API; 2 | using StructureHelper.Content.GUI; 3 | using StructureHelper.Models; 4 | using Terraria.DataStructures; 5 | using Terraria.ID; 6 | 7 | namespace StructureHelper.Content.Items 8 | { 9 | class StructureWand : ModItem 10 | { 11 | public bool secondPoint; 12 | 13 | public Point16 point1; 14 | public Point16 point2; 15 | 16 | public bool movePoint1; 17 | public bool movePoint2; 18 | 19 | public Point16 TopLeft => new(point1.X < point2.X ? point1.X : point2.X, point1.Y < point2.Y ? point1.Y : point2.Y); 20 | public Point16 BottomRight => new(point1.X > point2.X ? point1.X : point2.X, point1.Y > point2.Y ? point1.Y : point2.Y); 21 | public int Width => BottomRight.X - TopLeft.X; 22 | public int Height => BottomRight.Y - TopLeft.Y; 23 | 24 | public bool Ready => !secondPoint && point1 != default; 25 | 26 | public override string Texture => "StructureHelper/Assets/Items/" + Name; 27 | 28 | public override void SetStaticDefaults() 29 | { 30 | DisplayName.SetDefault("Structure Wand"); 31 | Tooltip.SetDefault("Select 2 points in the world, then right click to save a structure"); 32 | } 33 | 34 | public override void SetDefaults() 35 | { 36 | Item.useStyle = ItemUseStyleID.Swing; 37 | Item.useTime = 20; 38 | Item.useAnimation = 20; 39 | Item.rare = ItemRarityID.Blue; 40 | Item.noMelee = true; 41 | Item.noUseGraphic = true; 42 | } 43 | 44 | public override bool AltFunctionUse(Player player) 45 | { 46 | return true; 47 | } 48 | 49 | public override void HoldItem(Player player) 50 | { 51 | if (movePoint1) 52 | point1 = (Main.MouseWorld / 16).ToPoint16(); 53 | 54 | if (movePoint2) 55 | point2 = (Main.MouseWorld / 16).ToPoint16(); 56 | 57 | if (!Main.mouseLeft) 58 | { 59 | movePoint1 = false; 60 | movePoint2 = false; 61 | } 62 | } 63 | 64 | /// 65 | /// What happens when you right click after a finazed rectangle is present 66 | /// 67 | public virtual void OnConfirmRectangle() 68 | { 69 | NameConfirmPopup.OpenConfirmation((name) => 70 | { 71 | Saver.SaveToFile(StructureData.FromWorld(TopLeft.X, TopLeft.Y, Width, Height), null, name); 72 | }); 73 | 74 | 75 | //NameConfirmPopup.OpenConfirmation((name) => LegacySaver.SaveToFile(new Rectangle(TopLeft.X, TopLeft.Y, Width - 1, Height - 1), name: name)); 76 | } 77 | 78 | public override bool? UseItem(Player player) 79 | { 80 | if (player.altFunctionUse == 2 && Ready) 81 | { 82 | OnConfirmRectangle(); 83 | return true; 84 | } 85 | 86 | if (Ready) 87 | { 88 | if (Vector2.Distance(Main.MouseWorld, point1.ToVector2() * 16) <= 32) 89 | { 90 | movePoint1 = true; 91 | return true; 92 | } 93 | 94 | if (Vector2.Distance(Main.MouseWorld, point2.ToVector2() * 16) <= 32) 95 | { 96 | movePoint2 = true; 97 | return true; 98 | } 99 | } 100 | 101 | if (!secondPoint) 102 | { 103 | point1 = (Main.MouseWorld / 16).ToPoint16(); 104 | point2 = default; 105 | 106 | Main.NewText("Select Second Point"); 107 | secondPoint = true; 108 | } 109 | else 110 | { 111 | point2 = (Main.MouseWorld / 16).ToPoint16(); 112 | 113 | Main.NewText("Ready to save! Right click to save this structure..."); 114 | secondPoint = false; 115 | } 116 | 117 | return true; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Content/Items/TestWand.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Content.GUI; 2 | using Terraria.DataStructures; 3 | using Terraria.ID; 4 | 5 | namespace StructureHelper.Content.Items 6 | { 7 | class TestWand : ModItem 8 | { 9 | public static bool ignoreNulls = false; 10 | public static bool UIVisible; 11 | 12 | public override string Texture => "StructureHelper/Assets/Items/" + Name; 13 | 14 | public override void Load() 15 | { 16 | On_Main.DrawPlayers_AfterProjectiles += DrawPreview; 17 | } 18 | 19 | public override void SetStaticDefaults() 20 | { 21 | DisplayName.SetDefault("Structure Placer Wand"); 22 | Tooltip.SetDefault("left click to place the selected structure, right click to open the structure selector"); 23 | } 24 | 25 | public override void SetDefaults() 26 | { 27 | Item.useStyle = ItemUseStyleID.Swing; 28 | Item.useTime = 20; 29 | Item.useAnimation = 20; 30 | Item.rare = ItemRarityID.Blue; 31 | } 32 | 33 | public override bool AltFunctionUse(Player player) 34 | { 35 | return true; 36 | } 37 | 38 | public override bool? UseItem(Player player) 39 | { 40 | if (player.altFunctionUse == 2) 41 | { 42 | ManualGeneratorMenu.LoadStructures(); 43 | UIVisible = !UIVisible; 44 | return true; 45 | } 46 | 47 | if (ManualGeneratorMenu.selected != null) 48 | { 49 | var pos = new Point16(Player.tileTargetX, Player.tileTargetY); 50 | 51 | if (ManualGeneratorMenu.multiMode) 52 | API.MultiStructureGenerator.GenerateMultistructureSpecific(ManualGeneratorMenu.selected.path, ManualGeneratorMenu.multiIndex, pos, StructureHelper.Instance, true, ManualGeneratorMenu.ignoreNulls, ManualGeneratorMenu.flags); 53 | else 54 | API.Generator.GenerateStructure(ManualGeneratorMenu.selected.path, pos, StructureHelper.Instance, true, ManualGeneratorMenu.ignoreNulls, ManualGeneratorMenu.flags); 55 | } 56 | else 57 | { 58 | Main.NewText("No structure selected! Right click and select a structure from the menu to generate it.", Color.Red); 59 | } 60 | 61 | return true; 62 | } 63 | 64 | private void DrawPreview(On_Main.orig_DrawPlayers_AfterProjectiles orig, Main self) 65 | { 66 | orig(self); 67 | 68 | if (ManualGeneratorMenu.selected != null && ManualGeneratorMenu.preview != null && Main.LocalPlayer.HeldItem.type == Item.type) 69 | { 70 | Main.spriteBatch.Begin(default, default, default, default, default, default, Main.GameViewMatrix.TransformationMatrix); 71 | 72 | var pos = new Point16(Player.tileTargetX, Player.tileTargetY); 73 | Vector2 pos2 = pos.ToVector2() * 16 - Main.screenPosition; 74 | 75 | Helpers.GUIHelper.DrawBox(Main.spriteBatch, new Rectangle((int)pos2.X - 4, (int)pos2.Y - 4, ManualGeneratorMenu.preview.Width + 8, ManualGeneratorMenu.preview.Height + 8), Color.Red * 0.5f); 76 | 77 | if (ManualGeneratorMenu.preview?.preview != null) 78 | Main.spriteBatch.Draw(ManualGeneratorMenu.preview?.preview, pos2, Color.White * 0.5f); 79 | 80 | Main.spriteBatch.End(); 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Content/Tiles/NullBlock.cs: -------------------------------------------------------------------------------- 1 | using Terraria.ID; 2 | 3 | namespace StructureHelper.Content.Tiles 4 | { 5 | class NullBlockFraming : ModSystem 6 | { 7 | public override void Load() 8 | { 9 | On_WorldGen.SlopeTile += SlopeTileHook; 10 | } 11 | 12 | private static bool SlopeTileHook(On_WorldGen.orig_SlopeTile orig, int i, int j, int slope, bool noEffects) 13 | { 14 | bool isNeighborNull = false; 15 | 16 | isNeighborNull |= Framing.GetTileSafely(i + 1, j).TileType == ModContent.TileType(); 17 | isNeighborNull |= Framing.GetTileSafely(i - 1, j).TileType == ModContent.TileType(); 18 | isNeighborNull |= Framing.GetTileSafely(i, j + 1).TileType == ModContent.TileType(); 19 | isNeighborNull |= Framing.GetTileSafely(i, j - 1).TileType == ModContent.TileType(); 20 | 21 | if (isNeighborNull) 22 | return false; 23 | 24 | return orig(i, j, slope, noEffects); 25 | } 26 | } 27 | 28 | class NullBlock : ModTile 29 | { 30 | public override string Texture => "StructureHelper/Assets/Tiles/" + Name; 31 | 32 | public override void SetStaticDefaults() 33 | { 34 | Main.tileSolid[Type] = true; 35 | TileID.Sets.DrawsWalls[Type] = true; 36 | } 37 | 38 | public override bool Slope(int i, int j) { return false; } 39 | 40 | public override bool CanDrop(int i, int j) { return false; } 41 | } 42 | 43 | class NullWall : ModWall 44 | { 45 | public override string Texture => "StructureHelper/Assets/Tiles/" + Name; 46 | 47 | public override bool Drop(int i, int j, ref int type) { return false; } 48 | } 49 | 50 | class NullBlockItem : ModItem 51 | { 52 | public override string Texture => "StructureHelper/Assets/Tiles/" + Name; 53 | 54 | public override void SetStaticDefaults() 55 | { 56 | DisplayName.SetDefault("Null Block"); 57 | Tooltip.SetDefault("Use these in a structure to indicate where the generator\n should leave whatever already exists in the world untouched\n ignores walls, use null walls for that :3"); 58 | } 59 | 60 | public override void SetDefaults() 61 | { 62 | Item.width = 16; 63 | Item.height = 16; 64 | Item.maxStack = 1; 65 | Item.useTurn = true; 66 | Item.autoReuse = true; 67 | Item.useAnimation = 2; 68 | Item.useTime = 2; 69 | Item.useStyle = ItemUseStyleID.Swing; 70 | Item.rare = ItemRarityID.Yellow; 71 | Item.createTile = ModContent.TileType(); 72 | } 73 | } 74 | 75 | class NullWallItem : ModItem 76 | { 77 | public override string Texture => "StructureHelper/Assets/Tiles/" + Name; 78 | 79 | public override void SetStaticDefaults() 80 | { 81 | DisplayName.SetDefault("Null Wall"); 82 | Tooltip.SetDefault("Use these in a structure to indicate where the generator\n should leave walls that already exists in the world untouched\n for walls only, use null blocks for other things"); 83 | } 84 | 85 | public override void SetDefaults() 86 | { 87 | Item.width = 16; 88 | Item.height = 16; 89 | Item.maxStack = 1; 90 | Item.useTurn = true; 91 | Item.autoReuse = true; 92 | Item.useAnimation = 2; 93 | Item.useTime = 2; 94 | Item.useStyle = ItemUseStyleID.Swing; 95 | Item.rare = ItemRarityID.Yellow; 96 | Item.createWall = ModContent.WallType(); 97 | } 98 | } 99 | 100 | class NullTileAndWallPlacer : ModItem 101 | { 102 | public override string Texture => "StructureHelper/Assets/Tiles/" + Name; 103 | 104 | public override void SetStaticDefaults() 105 | { 106 | DisplayName.SetDefault("Null Tile Place-O-Matic"); 107 | Tooltip.SetDefault("Places a null tile and null wall at the same time!"); 108 | } 109 | 110 | public override void SetDefaults() 111 | { 112 | Item.width = 16; 113 | Item.height = 16; 114 | Item.maxStack = 1; 115 | Item.useTurn = true; 116 | Item.autoReuse = true; 117 | Item.useAnimation = 2; 118 | Item.useTime = 2; 119 | Item.useStyle = ItemUseStyleID.Swing; 120 | Item.rare = ItemRarityID.Yellow; 121 | Item.createTile = ModContent.TileType(); 122 | Item.createWall = ModContent.WallType(); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Core/Loaders/UILoading/SmartUIElement.cs: -------------------------------------------------------------------------------- 1 | using Terraria.UI; 2 | 3 | namespace StructureHelper.Core.Loaders.UILoading 4 | { 5 | /// 6 | /// Wrapper class that enforces Safe interoperability between event listeners and virtual methods for UI elements 7 | /// 8 | public class SmartUIElement : UIElement 9 | { 10 | #region XButton1 11 | /// 12 | /// A Safe wrapper around XButton1MouseUp that allows both an override and the OnXButton1MouseUp event to be used together 13 | /// 14 | /// The mouse event that occured to fire this listener 15 | public virtual void SafeXButton1MouseUp(UIMouseEvent evt) { } 16 | 17 | public sealed override void XButton1MouseUp(UIMouseEvent evt) 18 | { 19 | base.XButton1MouseUp(evt); 20 | SafeXButton1MouseUp(evt); 21 | } 22 | 23 | /// 24 | /// A Safe wrapper around XButton1MouseDown that allows both an override and the OnXButton1MouseDown event to be used together 25 | /// 26 | /// The mouse event that occured to fire this listener 27 | public virtual void SafeXButton1MouseDown(UIMouseEvent evt) { } 28 | 29 | public sealed override void XButton1MouseDown(UIMouseEvent evt) 30 | { 31 | base.XButton1MouseDown(evt); 32 | SafeXButton1MouseDown(evt); 33 | } 34 | 35 | /// 36 | /// A Safe wrapper around XButton1Click that allows both an override and the OnXButton1Click event to be used together 37 | /// 38 | /// The mouse event that occured to fire this listener 39 | public virtual void SafeXButton1Click(UIMouseEvent evt) { } 40 | 41 | public sealed override void XButton1Click(UIMouseEvent evt) 42 | { 43 | base.XButton1Click(evt); 44 | SafeXButton1Click(evt); 45 | } 46 | 47 | /// 48 | /// A Safe wrapper around XButton1DoubleClick that allows both an override and the OnXButton1DoubleClick event to be used together 49 | /// 50 | /// The mouse event that occured to fire this listener 51 | public virtual void SafeXButton1DoubleClick(UIMouseEvent evt) { } 52 | 53 | public sealed override void XButton1DoubleClick(UIMouseEvent evt) 54 | { 55 | base.XButton1DoubleClick(evt); 56 | SafeXButton1DoubleClick(evt); 57 | } 58 | #endregion 59 | 60 | #region XButton2 61 | /// 62 | /// A Safe wrapper around XButton2MouseUp that allows both an override and the OnXButton2MouseUp event to be used together 63 | /// 64 | /// The mouse event that occured to fire this listener 65 | public virtual void SafeXButton2MouseUp(UIMouseEvent evt) { } 66 | 67 | public sealed override void XButton2MouseUp(UIMouseEvent evt) 68 | { 69 | base.XButton2MouseUp(evt); 70 | SafeXButton2MouseUp(evt); 71 | } 72 | 73 | /// 74 | /// A Safe wrapper around XButton2MouseDown that allows both an override and the OnXButton2MouseDown event to be used together 75 | /// 76 | /// The mouse event that occured to fire this listener 77 | public virtual void SafeXButton2MouseDown(UIMouseEvent evt) { } 78 | 79 | public sealed override void XButton2MouseDown(UIMouseEvent evt) 80 | { 81 | base.XButton2MouseDown(evt); 82 | SafeXButton2MouseDown(evt); 83 | } 84 | 85 | /// 86 | /// A Safe wrapper around XButton2Click that allows both an override and the OnXButton2Click event to be used together 87 | /// 88 | /// The mouse event that occured to fire this listener 89 | public virtual void SafeXButton2Click(UIMouseEvent evt) { } 90 | 91 | public sealed override void XButton2Click(UIMouseEvent evt) 92 | { 93 | base.XButton2Click(evt); 94 | SafeXButton2Click(evt); 95 | } 96 | 97 | /// 98 | /// A Safe wrapper around XButton2DoubleClick that allows both an override and the OnXButton2DoubleClick event to be used together 99 | /// 100 | /// The mouse event that occured to fire this listener 101 | public virtual void SafeXButton2DoubleClick(UIMouseEvent evt) { } 102 | 103 | public sealed override void XButton2DoubleClick(UIMouseEvent evt) 104 | { 105 | base.XButton2DoubleClick(evt); 106 | SafeXButton2DoubleClick(evt); 107 | } 108 | #endregion 109 | 110 | #region LMB 111 | /// 112 | /// A Safe wrapper around MouseUp that allows both an override and the OnMouseUp event to be used together 113 | /// 114 | /// The mouse event that occured to fire this listener 115 | public virtual void SafeMouseUp(UIMouseEvent evt) { } 116 | 117 | public sealed override void LeftMouseUp(UIMouseEvent evt) 118 | { 119 | base.LeftMouseUp(evt); 120 | SafeMouseUp(evt); 121 | } 122 | 123 | /// 124 | /// A Safe wrapper around MouseDown that allows both an override and the OnMouseDown event to be used together 125 | /// 126 | /// The mouse event that occured to fire this listener 127 | public virtual void SafeMouseDown(UIMouseEvent evt) { } 128 | 129 | public sealed override void LeftMouseDown(UIMouseEvent evt) 130 | { 131 | base.LeftMouseDown(evt); 132 | SafeMouseDown(evt); 133 | } 134 | 135 | /// 136 | /// A Safe wrapper around Click that allows both an override and the OnClick event to be used together 137 | /// 138 | /// The mouse event that occured to fire this listener 139 | public virtual void SafeClick(UIMouseEvent evt) { } 140 | 141 | public sealed override void LeftClick(UIMouseEvent evt) 142 | { 143 | base.LeftClick(evt); 144 | SafeClick(evt); 145 | } 146 | 147 | /// 148 | /// A Safe wrapper around DoubleClick that allows both an override and the OnDoubleClick event to be used together 149 | /// 150 | /// The mouse event that occured to fire this listener 151 | public virtual void SafeDoubleClick(UIMouseEvent evt) { } 152 | 153 | public sealed override void LeftDoubleClick(UIMouseEvent evt) 154 | { 155 | base.LeftDoubleClick(evt); 156 | SafeDoubleClick(evt); 157 | } 158 | #endregion 159 | 160 | #region RMB 161 | /// 162 | /// A Safe wrapper around RightMouseUp that allows both an override and the OnRightMouseUp event to be used together 163 | /// 164 | /// The mouse event that occured to fire this listener 165 | public virtual void SafeRightMouseUp(UIMouseEvent evt) { } 166 | 167 | public sealed override void RightMouseUp(UIMouseEvent evt) 168 | { 169 | base.RightMouseUp(evt); 170 | SafeRightMouseUp(evt); 171 | } 172 | 173 | /// 174 | /// A Safe wrapper around RightMouseDown that allows both an override and the OnRightMouseDown event to be used together 175 | /// 176 | /// The mouse event that occured to fire this listener 177 | public virtual void SafeRightMouseDown(UIMouseEvent evt) { } 178 | 179 | public sealed override void RightMouseDown(UIMouseEvent evt) 180 | { 181 | base.RightMouseDown(evt); 182 | SafeRightMouseDown(evt); 183 | } 184 | 185 | /// 186 | /// A Safe wrapper around RightClick that allows both an override and the OnRightClick event to be used together 187 | /// 188 | /// The mouse event that occured to fire this listener 189 | public virtual void SafeRightClick(UIMouseEvent evt) { } 190 | 191 | public sealed override void RightClick(UIMouseEvent evt) 192 | { 193 | base.RightClick(evt); 194 | SafeRightClick(evt); 195 | } 196 | 197 | /// 198 | /// A Safe wrapper around RightDoubleClick that allows both an override and the OnRightDoubleClick event to be used together 199 | /// 200 | /// The mouse event that occured to fire this listener 201 | public virtual void SafeRightDoubleClick(UIMouseEvent evt) { } 202 | 203 | public sealed override void RightDoubleClick(UIMouseEvent evt) 204 | { 205 | base.RightDoubleClick(evt); 206 | SafeRightDoubleClick(evt); 207 | } 208 | #endregion 209 | 210 | #region MMB 211 | /// 212 | /// A Safe wrapper around MiddleMouseUp that allows both an override and the OnMiddleMouseUp event to be used together 213 | /// 214 | /// The mouse event that occured to fire this listener 215 | public virtual void SafeMiddleMouseUp(UIMouseEvent evt) { } 216 | 217 | public sealed override void MiddleMouseUp(UIMouseEvent evt) 218 | { 219 | base.MiddleMouseUp(evt); 220 | SafeMiddleMouseUp(evt); 221 | } 222 | 223 | /// 224 | /// A Safe wrapper around MiddleMouseDown that allows both an override and the OnMiddleMouseDown event to be used together 225 | /// 226 | /// The mouse event that occured to fire this listener 227 | public virtual void SafeMiddleMouseDown(UIMouseEvent evt) { } 228 | 229 | public sealed override void MiddleMouseDown(UIMouseEvent evt) 230 | { 231 | base.MiddleMouseDown(evt); 232 | SafeMiddleMouseDown(evt); 233 | } 234 | 235 | /// 236 | /// A Safe wrapper around MiddleClick that allows both an override and the OnMiddleClick event to be used together 237 | /// 238 | /// The mouse event that occured to fire this listener 239 | public virtual void SafeMiddleClick(UIMouseEvent evt) { } 240 | 241 | public sealed override void MiddleClick(UIMouseEvent evt) 242 | { 243 | base.MiddleClick(evt); 244 | SafeMiddleClick(evt); 245 | } 246 | 247 | /// 248 | /// A Safe wrapper around MiddleDoubleClick that allows both an override and the OnMiddleDoubleClick event to be used together 249 | /// 250 | /// The mouse event that occured to fire this listener 251 | public virtual void SafeMiddleDoubleClick(UIMouseEvent evt) { } 252 | 253 | public sealed override void MiddleDoubleClick(UIMouseEvent evt) 254 | { 255 | base.MiddleDoubleClick(evt); 256 | SafeMiddleDoubleClick(evt); 257 | } 258 | #endregion 259 | 260 | #region Misc 261 | /// 262 | /// A Safe wrapper around MouseOver that allows both an override and the OnMouseOver event to be used together 263 | /// 264 | /// The mouse event that occured to fire this listener 265 | public virtual void SafeMouseOver(UIMouseEvent evt) { } 266 | 267 | public sealed override void MouseOver(UIMouseEvent evt) 268 | { 269 | base.MouseOver(evt); 270 | SafeMouseOver(evt); 271 | } 272 | 273 | /// 274 | /// A Safe wrapper around Update that allows both an override and the OnUpdate event to be used together 275 | /// 276 | /// The mouse event that occured to fire this listener 277 | public virtual void SafeUpdate(GameTime gameTime) { } 278 | 279 | public sealed override void Update(GameTime gameTime) 280 | { 281 | base.Update(gameTime); 282 | SafeUpdate(gameTime); 283 | } 284 | 285 | /// 286 | /// A Safe wrapper around ScrollWheel that allows both an override and the OnScrollWheel event to be used together 287 | /// 288 | /// The mouse event that occured to fire this listener 289 | public virtual void SafeScrollWheel(UIScrollWheelEvent evt) { } 290 | 291 | public sealed override void ScrollWheel(UIScrollWheelEvent evt) 292 | { 293 | base.ScrollWheel(evt); 294 | SafeScrollWheel(evt); 295 | } 296 | #endregion 297 | } 298 | } -------------------------------------------------------------------------------- /Core/Loaders/UILoading/UILoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Terraria.UI; 5 | 6 | namespace StructureHelper.Core.Loaders.UILoading 7 | { 8 | /// 9 | /// Automatically loads SmartUIStates ala IoC. 10 | /// 11 | class UILoader : ModSystem 12 | { 13 | /// 14 | /// The collection of automatically craetaed UserInterfaces for SmartUIStates. 15 | /// 16 | public static List UserInterfaces = []; 17 | 18 | /// 19 | /// The collection of all automatically loaded SmartUIStates. 20 | /// 21 | public static List UIStates = []; 22 | 23 | /// 24 | /// Uses reflection to scan through and find all types extending SmartUIState that arent abstract, and loads an instance of them. 25 | /// 26 | public override void Load() 27 | { 28 | if (Main.dedServ) 29 | return; 30 | 31 | UserInterfaces = []; 32 | UIStates = []; 33 | 34 | foreach (Type t in Mod.Code.GetTypes()) 35 | { 36 | if (!t.IsAbstract && t.IsSubclassOf(typeof(SmartUIState))) 37 | { 38 | var state = (SmartUIState)Activator.CreateInstance(t, null); 39 | var userInterface = new UserInterface(); 40 | userInterface.SetState(state); 41 | state.UserInterface = userInterface; 42 | 43 | UIStates?.Add(state); 44 | UserInterfaces?.Add(userInterface); 45 | } 46 | } 47 | } 48 | 49 | public override void Unload() 50 | { 51 | UIStates.ForEach(n => n.Unload()); 52 | UserInterfaces = null; 53 | UIStates = null; 54 | } 55 | 56 | /// 57 | /// Helper method for creating and inserting a LegacyGameInterfaceLayer automatically 58 | /// 59 | /// The vanilla layers 60 | /// the UIState to bind to the layer 61 | /// Where this layer should be inserted 62 | /// The logic dictating the visibility of this layer 63 | /// The scale settings this layer should scale with 64 | public static void AddLayer(List layers, UIState state, int index, bool visible, InterfaceScaleType scale) 65 | { 66 | string name = state == null ? "Unknown" : state.ToString(); 67 | layers.Insert(index, new LegacyGameInterfaceLayer("BrickAndMortar: " + name, 68 | delegate 69 | { 70 | if (visible) 71 | state.Draw(Main.spriteBatch); 72 | 73 | return true; 74 | }, scale)); 75 | } 76 | 77 | /// 78 | /// Handles updating the UI states correctly 79 | /// 80 | /// 81 | public override void UpdateUI(GameTime gameTime) 82 | { 83 | if (Main.ingameOptionsWindow || Main.InGameUI.IsVisible) 84 | return; 85 | foreach (UserInterface eachState in UserInterfaces) 86 | { 87 | if (eachState?.CurrentState != null && ((SmartUIState)eachState.CurrentState).Visible) 88 | eachState.Update(gameTime); 89 | } 90 | } 91 | 92 | /// 93 | /// Gets the autoloaded SmartUIState instance for a given SmartUIState subclass 94 | /// 95 | /// The SmartUIState subclass to get the instance of 96 | /// The autoloaded instance of the desired SmartUIState 97 | public static T GetUIState() where T : SmartUIState 98 | { 99 | return UIStates.FirstOrDefault(n => n is T) as T; 100 | } 101 | 102 | /// 103 | /// Forcibly reloads a SmartUIState and it's associated UserInterface 104 | /// 105 | /// The SmartUIState subclass to reload 106 | public static void ReloadState() where T : SmartUIState 107 | { 108 | int index = UIStates.IndexOf(GetUIState()); 109 | UIStates[index] = (T)Activator.CreateInstance(typeof(T), null); 110 | UserInterfaces[index] = new UserInterface(); 111 | UserInterfaces[index].SetState(UIStates[index]); 112 | } 113 | 114 | /// 115 | /// Handles the insertion of the automatically generated UIs 116 | /// 117 | /// 118 | public override void ModifyInterfaceLayers(List layers) 119 | { 120 | for (int k = 0; k < UIStates.Count; k++) 121 | { 122 | SmartUIState state = UIStates[k]; 123 | AddLayer(layers, state, state.InsertionIndex(layers), state.Visible, state.Scale); 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /Generator.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using StructureHelper.Helpers; 3 | using StructureHelper.Models; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using Terraria.DataStructures; 8 | using Terraria.ID; 9 | using Terraria.ModLoader.IO; 10 | 11 | namespace StructureHelper 12 | { 13 | /// 14 | /// The legacy API left as is to prevent runtime breakage. 15 | /// 16 | public static class Generator 17 | { 18 | /// 19 | /// This method generates a structure from a structure file within your mod. 20 | /// 21 | /// The path to your structure file within your mod - this should not include your mod's folder, only the path beyond it. 22 | /// The position in the world in which you want your structure to generate, in tile coordinates. 23 | /// The instance of your mod to grab the file from. 24 | /// Indicates if you want to use a fully qualified path to get the structure file instead of one from your mod - generally should only be used for debugging. 25 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll reference. 26 | /// Allows you to pass flags for special generation behavior. See 27 | /// If the structure generated successfully or not 28 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 29 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 30 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 31 | public static bool GenerateStructure(string path, Point16 pos, Mod mod, bool fullPath = false, bool ignoreNull = false, GenFlags flags = GenFlags.None) 32 | { 33 | return API.Legacy.LegacyGenerator.GenerateStructure(path, pos, mod, fullPath, ignoreNull, flags); 34 | } 35 | 36 | /// 37 | /// This method generates a structure selected randomly from a multistructure file within your mod. 38 | /// 39 | /// The path to your multistructure file within your mod - this should not include your mod's folder, only the path beyond it. 40 | /// The position in the world in which you want your structure to generate, in tile coordinates. 41 | /// The instance of your mod to grab the file from. 42 | /// Indicates if you want to use a fully qualified path to get the structure file instead of one from your mod - generally should only be used for debugging. 43 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll refference. 44 | /// Allows you to pass flags for special generation behavior. See 45 | /// If the structure generated successfully or not 46 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 47 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 48 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 49 | public static bool GenerateMultistructureRandom(string path, Point16 pos, Mod mod, bool fullPath = false, bool ignoreNull = false, GenFlags flags = GenFlags.None) 50 | { 51 | return API.Legacy.LegacyGenerator.GenerateMultistructureRandom(path, pos, mod, fullPath, ignoreNull, flags); 52 | } 53 | 54 | /// 55 | /// This method generates a structure you select from a multistructure file within your mod. Useful if you want to do your own weighted randomization or want additional logic based on dimensions gotten from GetMultistructureDimensions. 56 | /// 57 | /// The path to your multistructure file within your mod - this should not include your mod's folder, only the path beyond it. 58 | /// The position in the world in which you want your structure to generate, in tile coordinates. 59 | /// The instance of your mod to grab the file from. 60 | /// The index of the structure you want to generate out of the multistructure file, structure indicies are 0-based and match the order they were saved in. 61 | /// Indicates if you want to use a fully qualified path to get the structure file instead of one from your mod - generally should only be used for debugging. 62 | /// If the structure should repsect the normal behavior of null tiles or not. This should never be true if you're using the mod as a dll refference. 63 | /// Allows you to pass flags for special generation behavior. See 64 | /// If the structure generated successfully or not 65 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 66 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 67 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 68 | public static bool GenerateMultistructureSpecific(string path, Point16 pos, Mod mod, int index, bool fullPath = false, bool ignoreNull = false, GenFlags flags = GenFlags.None) 69 | { 70 | return API.Legacy.LegacyGenerator.GenerateMultistructureSpecific(path, pos, mod, index, fullPath, ignoreNull, flags); 71 | } 72 | 73 | /// 74 | /// Gets the dimensions of a structure from a structure file within your mod. 75 | /// 76 | /// The path to your structure file within your mod - this should not include your mod's folder, only the path beyond it. 77 | /// The instance of your mod to grab the file from. 78 | /// The Point16 variable which you want to be set to the dimensions of the structure. 79 | /// Indicates if you want to use a fully qualified path to get the structure file instead of one from your mod - generally should only be used for debugging. 80 | /// 81 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 82 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 83 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 84 | public static bool GetDimensions(string path, Mod mod, ref Point16 dims, bool fullPath = false) 85 | { 86 | return API.Legacy.LegacyGenerator.GetDimensions(path, mod, ref dims, fullPath); 87 | } 88 | 89 | /// 90 | /// Gets the dimensions of a structure from a structure file within your mod. 91 | /// 92 | /// The path to your structure file within your mod - this should not include your mod's folder, only the path beyond it. 93 | /// The instance of your mod to grab the file from. 94 | /// The index of the structure you want to get the dimensions of out of the multistructure file, structure indicies are 0-based and match the order they were saved in. 95 | /// The Point16 variable which you want to be set to the dimensions of the structure. 96 | /// Indicates if you want to use a fully qualified path to get the structure file instead of one from your mod - generally should only be used for debugging. 97 | /// 98 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 99 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 100 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 101 | public static bool GetMultistructureDimensions(string path, Mod mod, int index, ref Point16 dims, bool fullPath = false) 102 | { 103 | return API.Legacy.LegacyGenerator.GetMultistructureDimensions(path, mod, index, ref dims, fullPath); 104 | } 105 | 106 | /// 107 | /// Checks if a structure file is a multistructure or not. Can be used to easily add support for parameterizing strucutres or multistructures in your mod. 108 | /// 109 | /// The path to the structure file you wish to check. 110 | /// The instance of your mod to grab the file from. 111 | /// True if the file is a multistructure, False if the file is a structure, null if it is invalid. 112 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 113 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 114 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 115 | public static bool? IsMultistructure(string path, Mod mod) 116 | { 117 | return API.Legacy.LegacyGenerator.IsMultistructure(path, mod); 118 | } 119 | 120 | /// 121 | /// Gets the quantity of structures in a multistructure file if possible. Returns null if the structure is invalid or not a multistructure. 122 | /// 123 | /// The path to the structure file you wish to check. 124 | /// The instance of your mod to grab the file from. 125 | /// The amount of structures in a multistructure, or null if invalid. 126 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 127 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 128 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 129 | public static int? GetStructureCount(string path, Mod mod) 130 | { 131 | return API.Legacy.LegacyGenerator.GetStructureCount(path, mod); 132 | } 133 | 134 | /// 135 | /// Parses and generates the actual tiles from a structure file 136 | /// 137 | /// The structure data TagCompound to generate from 138 | /// The position in the world of the top-leftmost tile to be placed at 139 | /// If this structure should place null tiles or not 140 | /// If the structure successfully generated or not 141 | [Obsolete("Legacy generation API is deprecated. Please port your structures to the 3.0 format and use StructureHelper.API.Generator." + 142 | "\n Porting instructions can be found at https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide" + 143 | "\n If you truly need legacy API access, use StructureHelper.API.Legacy.LegacyGenerator.", true)] 144 | public static unsafe bool Generate(TagCompound tag, Point16 pos, bool ignoreNull = false, GenFlags flags = GenFlags.None) 145 | { 146 | return API.Legacy.LegacyGenerator.Generate(tag, pos, ignoreNull, flags); 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace StructureHelper 5 | { 6 | static class Helper 7 | { 8 | /// 9 | /// Randomizes the order of a list using the Fischer-Yates algorithm 10 | /// 11 | /// The type of the list to randomize 12 | /// The list to randomize 13 | public static void RandomizeList(ref List input) 14 | { 15 | int n = input.Count(); 16 | 17 | while (n > 1) 18 | { 19 | n--; 20 | int k = WorldGen.genRand.Next(n + 1); 21 | (input[n], input[k]) = (input[k], input[n]); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Helpers/ErrorHelper.cs: -------------------------------------------------------------------------------- 1 | namespace StructureHelper.Helpers 2 | { 3 | internal class ErrorHelper 4 | { 5 | public static string GenerateErrorMessage(string message, Mod mod) 6 | { 7 | if (mod != null) 8 | return $"{mod.DisplayName}({mod.Name}) <-- has caused an issue with Structure helper! \n\n{message}\n\nIf you are a player, please report this to the developers of {mod.DisplayName}, NOT StructureHelper!"; 9 | else 10 | return $"An unknown mod has caused an issue with Structure helper! \n\n{message}\n\nIf you are a developer, did you pass null to a parameter requiring a mod? Otherwise this error may have occured in a place without mod context."; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Helpers/GUIHelper.cs: -------------------------------------------------------------------------------- 1 | using ReLogic.Graphics; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | using Terraria.ModLoader.Config; 8 | using Terraria.UI; 9 | 10 | namespace StructureHelper.Helpers 11 | { 12 | internal static class GUIHelper 13 | { 14 | /// 15 | /// Draws a simple box in the style of the StructureHelper GUI. 16 | /// 17 | /// the spriteBatch to draw the box with 18 | /// where/how big the box should be drawn 19 | /// 20 | public static void DrawBox(SpriteBatch spriteBatch, Rectangle target, Color color = default) 21 | { 22 | Texture2D tex = Assets.GUI.Box.Value; 23 | 24 | if (color == default) 25 | color = new Color(49, 84, 141) * 0.9f; 26 | 27 | var sourceCorner = new Rectangle(0, 0, 6, 6); 28 | var sourceEdge = new Rectangle(6, 0, 4, 6); 29 | var sourceCenter = new Rectangle(6, 6, 4, 4); 30 | 31 | Rectangle inner = target; 32 | inner.Inflate(-6, -6); 33 | 34 | spriteBatch.Draw(tex, inner, sourceCenter, color); 35 | 36 | spriteBatch.Draw(tex, new Rectangle(target.X + 6, target.Y, target.Width - 12, 6), sourceEdge, color, 0, Vector2.Zero, 0, 0); 37 | spriteBatch.Draw(tex, new Rectangle(target.X, target.Y - 6 + target.Height, target.Height - 12, 6), sourceEdge, color, -(float)Math.PI * 0.5f, Vector2.Zero, 0, 0); 38 | spriteBatch.Draw(tex, new Rectangle(target.X - 6 + target.Width, target.Y + target.Height, target.Width - 12, 6), sourceEdge, color, (float)Math.PI, Vector2.Zero, 0, 0); 39 | spriteBatch.Draw(tex, new Rectangle(target.X + target.Width, target.Y + 6, target.Height - 12, 6), sourceEdge, color, (float)Math.PI * 0.5f, Vector2.Zero, 0, 0); 40 | 41 | spriteBatch.Draw(tex, new Rectangle(target.X, target.Y, 6, 6), sourceCorner, color, 0, Vector2.Zero, 0, 0); 42 | spriteBatch.Draw(tex, new Rectangle(target.X + target.Width, target.Y, 6, 6), sourceCorner, color, (float)Math.PI * 0.5f, Vector2.Zero, 0, 0); 43 | spriteBatch.Draw(tex, new Rectangle(target.X + target.Width, target.Y + target.Height, 6, 6), sourceCorner, color, (float)Math.PI, Vector2.Zero, 0, 0); 44 | spriteBatch.Draw(tex, new Rectangle(target.X, target.Y + target.Height, 6, 6), sourceCorner, color, (float)Math.PI * 1.5f, Vector2.Zero, 0, 0); 45 | } 46 | 47 | /// 48 | /// Draws the outline of a box in the style of the DragonLens GUI. 49 | /// 50 | /// the spriteBatch to draw the outline with 51 | /// where/how big the outline should be drawn 52 | /// the color of the outline 53 | public static void DrawOutline(SpriteBatch spriteBatch, Rectangle target, Color color = default) 54 | { 55 | Texture2D tex = Assets.GUI.Box.Value; 56 | 57 | if (color == default) 58 | color = new Color(49, 84, 141) * 0.9f; 59 | 60 | var sourceCorner = new Rectangle(0, 0, 6, 6); 61 | var sourceEdge = new Rectangle(6, 0, 4, 6); 62 | var sourceCenter = new Rectangle(6, 6, 4, 4); 63 | 64 | spriteBatch.Draw(tex, new Rectangle(target.X + 6, target.Y, target.Width - 12, 6), sourceEdge, color, 0, Vector2.Zero, 0, 0); 65 | spriteBatch.Draw(tex, new Rectangle(target.X, target.Y - 6 + target.Height, target.Height - 12, 6), sourceEdge, color, -(float)Math.PI * 0.5f, Vector2.Zero, 0, 0); 66 | spriteBatch.Draw(tex, new Rectangle(target.X - 6 + target.Width, target.Y + target.Height, target.Width - 12, 6), sourceEdge, color, (float)Math.PI, Vector2.Zero, 0, 0); 67 | spriteBatch.Draw(tex, new Rectangle(target.X + target.Width, target.Y + 6, target.Height - 12, 6), sourceEdge, color, (float)Math.PI * 0.5f, Vector2.Zero, 0, 0); 68 | 69 | spriteBatch.Draw(tex, new Rectangle(target.X, target.Y, 6, 6), sourceCorner, color, 0, Vector2.Zero, 0, 0); 70 | spriteBatch.Draw(tex, new Rectangle(target.X + target.Width, target.Y, 6, 6), sourceCorner, color, (float)Math.PI * 0.5f, Vector2.Zero, 0, 0); 71 | spriteBatch.Draw(tex, new Rectangle(target.X + target.Width, target.Y + target.Height, 6, 6), sourceCorner, color, (float)Math.PI, Vector2.Zero, 0, 0); 72 | spriteBatch.Draw(tex, new Rectangle(target.X, target.Y + target.Height, 6, 6), sourceCorner, color, (float)Math.PI * 1.5f, Vector2.Zero, 0, 0); 73 | } 74 | 75 | /// 76 | /// Gets the inverse of a color. Used for 'constrasting' elements of the StructureHelper GUI, such as outlines indicating something being on. 77 | /// 78 | /// the color to invert 79 | /// the inverted color 80 | public static Color InvertColor(this Color color) 81 | { 82 | return new Color(255 - color.R, 255 - color.G, 255 - color.B, color.A); 83 | } 84 | 85 | /// 86 | /// Wraps a string to a given maximum width, by forcibly adding newlines. Normal newlines will be removed, put the text 'NEWBLOCK' in your string to break a paragraph if needed. 87 | /// 88 | /// The input string to be wrapped 89 | /// The maximum width of the text 90 | /// The font the text will be drawn in, to calculate its size 91 | /// The scale the text will be drawn at, to calculate its size 92 | /// Input text with linebreaks inserted so it obeys the width constraint. 93 | public static string WrapString(string input, int length, DynamicSpriteFont font, float scale) 94 | { 95 | string output = ""; 96 | 97 | // In case input is empty and causes an error, we put an empty string to the list 98 | var words = new List { "" }; 99 | 100 | // Word splitting, with CJK characters being treated as a single word 101 | string cacheString = ""; 102 | for (int i = 0; i < input.Length; i++) 103 | { 104 | // By doing this we split words, and make the first character of words always a space 105 | if (cacheString != string.Empty && char.IsWhiteSpace(input[i])) 106 | { 107 | words.Add(cacheString); 108 | cacheString = ""; 109 | } 110 | 111 | // Single CJK character just get directly added to the list 112 | if (LocalizationHelper.IsCjkCharacter(input[i])) 113 | { 114 | if (cacheString != string.Empty) 115 | { 116 | words.Add(cacheString); 117 | cacheString = ""; 118 | } 119 | 120 | // If the next character is a CJK punctuation, we add both characters as a single word 121 | // Unless the next character is a right close CJK punctuation (e.g. left brackets), in which case we add only the current character 122 | if (i + 1 < input.Length && LocalizationHelper.IsCjkPunctuation(input[i + 1]) && !LocalizationHelper.IsRightCloseCjkPunctuation(input[i + 1])) 123 | { 124 | words.Add(input[i].ToString() + input[i + 1]); 125 | i++; 126 | } 127 | else 128 | { 129 | words.Add(input[i].ToString()); 130 | } 131 | 132 | continue; 133 | } 134 | 135 | cacheString += input[i]; 136 | } 137 | 138 | // Add the last word 139 | if (!string.IsNullOrEmpty(cacheString)) 140 | { 141 | words.Add(cacheString); 142 | } 143 | 144 | string line = ""; 145 | foreach (string str in words) 146 | { 147 | if (str == " NEWBLOCK") 148 | { 149 | output += "\n\n"; 150 | line = ""; 151 | continue; 152 | } 153 | 154 | if (str == " NEWLN") 155 | { 156 | output += "\n"; 157 | line = ""; 158 | continue; 159 | } 160 | 161 | if (font.MeasureString(line).X * scale < length) 162 | { 163 | output += str; 164 | line += str; 165 | } 166 | else 167 | { 168 | // We don't want the first character of a line to be a space 169 | output += "\n" + str.TrimStart(); 170 | line = str; 171 | } 172 | } 173 | 174 | return output; 175 | } 176 | 177 | /// 178 | /// Uses reflection to forcibly open the TModLoader configuration UI to a given ModConfig's screen. 179 | /// 180 | /// The config to open up 181 | public static void OpenConfig(ModConfig config) 182 | { 183 | IngameFancyUI.CoverNextFrame(); 184 | Main.playerInventory = false; 185 | Main.editChest = false; 186 | Main.npcChatText = ""; 187 | Main.inFancyUI = true; 188 | 189 | Type interfaceType = typeof(ModLoader).Assembly.GetType("Terraria.ModLoader.UI.Interface"); 190 | FieldInfo modConfigList = interfaceType.GetField("modConfigList", BindingFlags.Static | BindingFlags.NonPublic); 191 | 192 | Type uiModConfig = typeof(ModLoader).Assembly.GetType("Terraria.ModLoader.Config.UI.UIModConfig"); 193 | FieldInfo modConfig = interfaceType.GetField("modConfig", BindingFlags.Static | BindingFlags.NonPublic); 194 | MethodInfo setMod = uiModConfig.GetMethod("SetMod", BindingFlags.Instance | BindingFlags.NonPublic); 195 | 196 | var ui = (UIState)modConfig.GetValue(null); 197 | 198 | setMod.Invoke(ui, new object[] { ModContent.GetInstance(), config }); 199 | Main.InGameUI.SetState(ui); 200 | } 201 | 202 | /// 203 | /// Opens a URL in the web browser 204 | /// 205 | /// The URL to open to 206 | public static void OpenUrl(string url) 207 | { 208 | try 209 | { 210 | Process.Start(url); 211 | } 212 | catch 213 | { 214 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 215 | { 216 | url = url.Replace("&", "^&"); 217 | Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); 218 | } 219 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 220 | { 221 | Process.Start("xdg-open", url); 222 | } 223 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 224 | { 225 | Process.Start("open", url); 226 | } 227 | else 228 | { 229 | throw; 230 | } 231 | } 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /Helpers/LocalizationHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Terraria.Localization; 3 | 4 | namespace StructureHelper.Helpers 5 | { 6 | internal static class LocalizationHelper 7 | { 8 | /// 9 | /// Gets a localized text value of the mod. 10 | /// If no localization is found, the key itself is returned. 11 | /// 12 | /// the localization key 13 | /// optional args that should be passed 14 | /// the text should be displayed 15 | public static string GetText(string key, params object[] args) 16 | { 17 | return Language.Exists($"Mods.StructureHelper.{key}") ? Language.GetTextValue($"Mods.StructureHelper.{key}", args) : key; 18 | } 19 | 20 | public static string GetGUIText(string key, params object[] args) 21 | { 22 | return GetText($"GUI.{key}", args); 23 | } 24 | 25 | public static string GetToolText(string key, params object[] args) 26 | { 27 | return GetText($"Tools.{key}", args); 28 | } 29 | 30 | public static bool IsCjkPunctuation(char a) 31 | { 32 | return Regex.IsMatch(a.ToString(), @"\p{IsCJKSymbolsandPunctuation}|\p{IsHalfwidthandFullwidthForms}"); 33 | } 34 | 35 | public static bool IsCjkUnifiedIdeographs(char a) 36 | { 37 | return Regex.IsMatch(a.ToString(), @"\p{IsCJKUnifiedIdeographs}"); 38 | } 39 | 40 | public static bool IsRightCloseCjkPunctuation(char a) 41 | { 42 | return a is '(' or '【' or '《' or '{' or '「' or '[' or '⦅' or '“'; 43 | } 44 | 45 | public static bool IsCjkCharacter(char a) 46 | { 47 | return IsCjkUnifiedIdeographs(a) || IsCjkPunctuation(a); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Localization/en-US.hjson: -------------------------------------------------------------------------------- 1 | Mods: { 2 | StructureHelper: { 3 | Items: { 4 | ChestWand: { 5 | DisplayName: Chest Wand 6 | Tooltip: 7 | ''' 8 | Right click to open the chest rule menu 9 | Left click a chest to set the current rules on it 10 | Right click a chest with rules to copy them 11 | ''' 12 | } 13 | 14 | MultistructureWand: { 15 | DisplayName: Multistructure Wand 16 | Tooltip: Select 2 points in the world, then right click to add a structure. Right click in your inventory when done to save. 17 | } 18 | 19 | StructureWand: { 20 | DisplayName: Structure Wand 21 | Tooltip: Select 2 points in the world, then right click to save a structure 22 | } 23 | 24 | TestWand: { 25 | DisplayName: Structure Placer Wand 26 | Tooltip: left click to place the selected structure, right click to open the structure selector 27 | } 28 | 29 | NullBlockItem: { 30 | DisplayName: Null Block 31 | Tooltip: 32 | ''' 33 | Use these in a structure to indicate where the generator 34 | should leave whatever already exists in the world untouched 35 | ignores walls, use null walls for that :3 36 | ''' 37 | } 38 | 39 | NullTileAndWallPlacer: { 40 | DisplayName: Null Tile Place-O-Matic 41 | Tooltip: Places a null tile and null wall at the same time! 42 | } 43 | 44 | NullWallItem: { 45 | DisplayName: Null Wall 46 | Tooltip: 47 | ''' 48 | Use these in a structure to indicate where the generator 49 | should leave walls that already exists in the world untouched 50 | for walls only, use null blocks for other things 51 | ''' 52 | } 53 | 54 | PortingWand: { 55 | DisplayName: Porting Wand 56 | Tooltip: Automatically ports every structure in your SavedStructures directory. 57 | } 58 | 59 | UnitWand: { 60 | DisplayName: The Wand 61 | Tooltip: A test for myself If I leave this in bonk me. 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LocalizationRoundabout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Terraria.Localization; 4 | 5 | namespace StructureHelper 6 | { 7 | internal class LocalizationRewriter : ModSystem 8 | { 9 | public override void PostSetupContent() 10 | { 11 | #if DEBUG 12 | MethodInfo refreshInfo = typeof(LocalizationLoader).GetMethod("UpdateLocalizationFilesForMod", BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(Mod), typeof(string), typeof(GameCulture) }); 13 | refreshInfo.Invoke(null, new object[] { StructureHelper.Instance, null, Language.ActiveCulture }); 14 | #endif 15 | } 16 | } 17 | 18 | internal static class LocalizationRoundabout 19 | { 20 | public static void SetDefault(this LocalizedText text, string value) 21 | { 22 | #if DEBUG 23 | PropertyInfo valueProp = typeof(LocalizedText).GetProperty("Value", BindingFlags.Public | BindingFlags.Instance); 24 | 25 | LanguageManager.Instance.GetOrRegister(text.Key, () => value); 26 | valueProp.SetValue(text, value); 27 | #endif 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Models/GenFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace StructureHelper 8 | { 9 | /// 10 | /// Flags that allow users to define special behavior for structure generation. 11 | /// 12 | [Flags] 13 | public enum GenFlags 14 | { 15 | /// 16 | /// No special generation 17 | /// 18 | None = 0b0, 19 | /// 20 | /// Null tiles will inherit the type of the tile behind them, but keep their slope if that tile is slopable 21 | /// 22 | NullsKeepGivenSlope = 0b1, 23 | /// 24 | /// Null tiles and walls will inherit the type of the tile/wall behind them, but will keep the paint they are given 25 | /// 26 | NullsKeepGivenPaint = 0b10, 27 | /// 28 | /// Tile entities will not have their saved data placed in the generated structures, instead falling back to acting as if they are newly created 29 | /// 30 | IgnoreTileEnttiyData = 0b100, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Models/Legacy/TileSaveData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Terraria.ModLoader.IO; 3 | 4 | namespace StructureHelper 5 | { 6 | /// 7 | /// A struct representing tile data to be saved/loaded from structure files. 8 | /// 9 | public struct TileSaveData : TagSerializable 10 | { 11 | /// 12 | /// The tile to be placed, either a number if a vanilla tile (ID), or a fully qualified internal name for modded tiles. 13 | /// 14 | public string tile; 15 | /// 16 | /// The wall to be placed, either a number if a vanilla wall (ID), or a fully qualified internal name for modded walls. 17 | /// 18 | public string wall; 19 | /// 20 | /// The X frame of a tile 21 | /// 22 | public short frameX; 23 | /// 24 | /// the Y frame of a tile 25 | /// 26 | public short frameY; 27 | /// 28 | /// One part of the packed vanilla data about a tile 29 | /// 30 | public int wallWireData; 31 | /// 32 | /// The other part of the packed vanilla data about a tile 33 | /// 34 | public short packedLiquidData; 35 | /// 36 | /// Data about certain coatings added by 1.4.4 37 | /// 38 | public byte brightInvisibleData; 39 | 40 | /// 41 | /// The fully qualiified name of a modded tile entity, if one should exist here 42 | /// 43 | public string TEType; 44 | /// 45 | /// The data associated with a tile entity associated here 46 | /// 47 | public TagCompound TEData; 48 | 49 | public static Func DESERIALIZER = DeserializeData; 50 | 51 | /// 52 | /// If the tile here is air or not. (Note that TileID 0 is dirt, this is all that differentiates air and dirt.) 53 | /// 54 | public bool Active => TileDataPacking.GetBit(wallWireData, 0); 55 | 56 | public TileSaveData(string tile, string wall, short frameX, short frameY, int wallWireData, short packedLiquidData, byte brightInvisibleData, string teType = "", TagCompound teData = null) 57 | { 58 | this.tile = tile; 59 | this.wall = wall; 60 | this.frameX = frameX; 61 | this.frameY = frameY; 62 | this.wallWireData = wallWireData; 63 | this.packedLiquidData = packedLiquidData; 64 | this.brightInvisibleData = brightInvisibleData; 65 | TEType = teType; 66 | TEData = teData; 67 | } 68 | 69 | /// 70 | /// Deserialize a TagCompound into an instance of this struct 71 | /// 72 | /// The tag to interpret 73 | /// The unpacked TileSaveData 74 | public static TileSaveData DeserializeData(TagCompound tag) 75 | { 76 | var output = new TileSaveData( 77 | tag.GetString("Tile"), 78 | tag.GetString("Wall"), 79 | tag.GetShort("FrameX"), 80 | tag.GetShort("FrameY"), 81 | 82 | tag.GetInt("WallWireData"), 83 | tag.GetShort("PackedLiquidData"), 84 | tag.TryGet("BrightInvisibleData", out byte brightInvis) ? brightInvis : (byte)0 85 | ); 86 | 87 | if (tag.ContainsKey("TEType")) 88 | { 89 | output.TEType = tag.GetString("TEType"); 90 | output.TEData = tag.Get("TEData"); 91 | } 92 | 93 | return output; 94 | } 95 | 96 | /// 97 | /// Serialize this struct into a TagCompound for saving 98 | /// 99 | /// The packed TagCompound 100 | public TagCompound SerializeData() 101 | { 102 | var tag = new TagCompound() 103 | { 104 | ["Tile"] = tile, 105 | ["Wall"] = wall, 106 | ["FrameX"] = frameX, 107 | ["FrameY"] = frameY, 108 | 109 | ["WallWireData"] = wallWireData, 110 | ["PackedLiquidData"] = packedLiquidData, 111 | ["BrightInvisibleData"] = brightInvisibleData 112 | }; 113 | 114 | if (TEType != "") 115 | { 116 | tag.Add("TEType", TEType); 117 | tag.Add("TEData", TEData); 118 | } 119 | 120 | return tag; 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /Models/MultiStructureData.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Helpers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace StructureHelper.Models 10 | { 11 | /// 12 | /// Represents the contents of a multi structure file in-memory. This object is only valid 13 | /// for a given mod load instance, as modded tile types will be parsed out from the table 14 | /// into the body data. If you are storing these and your mod reloads, you should consider 15 | /// them invalid afterwards. 16 | /// 17 | public class MultiStructureData 18 | { 19 | public const string HEADER_TEXT = "STRUCTURE_HELPER_MULTI_STRUCTURE"; 20 | 21 | /// 22 | /// The amount of structures in this multistructure 23 | /// 24 | public int count; 25 | 26 | /// 27 | /// The structure data in this multistructure 28 | /// 29 | public List structures = []; 30 | 31 | /// 32 | /// Constructs a MultiStructureData from raw binary data 33 | /// 34 | /// A reader for the raw binary data, such as from a file 35 | /// A MultiStructureData constructed from the raw bytes 36 | /// 37 | public static MultiStructureData FromStream(BinaryReader reader) 38 | { 39 | string headerText = reader.ReadString(); 40 | 41 | if (headerText != HEADER_TEXT) 42 | throw new InvalidDataException(ErrorHelper.GenerateErrorMessage("Attempted to deserialize binary data that is not a 3.0 multi structure file! Did you pass the path to a .shmstruct file? If so, did you change the file extension without actually porting your multi structure file from 2.0?", null)); 43 | 44 | var data = new MultiStructureData(); 45 | data.count = reader.ReadInt32(); 46 | 47 | for(int k = 0; k < data.count; k++) 48 | { 49 | data.structures.Add(StructureData.FromStream(reader)); 50 | } 51 | 52 | return data; 53 | } 54 | 55 | /// 56 | /// Generates a MultiStructureData from a list of StructureData 57 | /// 58 | /// The StructureData to put into this MultiStructureData 59 | /// The constructed MultiStructureData containing all elements in the source list 60 | public static MultiStructureData FromStructureList(List source) 61 | { 62 | MultiStructureData data = new(); 63 | data.count = source.Count; 64 | data.structures.AddRange(source); 65 | 66 | return data; 67 | } 68 | 69 | /// 70 | /// Serialize this MultiStructureData into a binary writer, such as to write to a file 71 | /// 72 | public void Serialize(BinaryWriter writer) 73 | { 74 | writer.Write(HEADER_TEXT); 75 | writer.Write(count); 76 | 77 | for(int k = 0; k < count; k++) 78 | { 79 | structures[k].Serialize(writer); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Models/NbtEntries/SignNBTEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Terraria.DataStructures; 7 | using Terraria.ModLoader.IO; 8 | 9 | namespace StructureHelper.Models.NbtEntries 10 | { 11 | internal class SignNBTEntry : StructureNBTEntry 12 | { 13 | public string text; 14 | 15 | public static Func DESERIALIZER = Deserialize; 16 | 17 | public SignNBTEntry(int x, int y, string text) : base(x, y) 18 | { 19 | this.text = text; 20 | } 21 | 22 | public static SignNBTEntry Deserialize(TagCompound tag) 23 | { 24 | SignNBTEntry entry = new 25 | (tag.GetInt("x"), 26 | tag.GetInt("y"), 27 | tag.GetString("text")); 28 | 29 | return entry; 30 | } 31 | 32 | public override void OnGenerate(Point16 generatingAt, bool ignoreNull, GenFlags flags) 33 | { 34 | int index = Sign.ReadSign(generatingAt.X, generatingAt.Y, true); 35 | 36 | // Index may be -1 if there are already 1000 signs in the world 37 | if (index != -1) 38 | Sign.TextSign(index, text); 39 | } 40 | 41 | public override void Serialize(TagCompound tag) 42 | { 43 | tag["text"] = text; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Models/NbtEntries/TileEntityNBTEntry.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.ChestHelper; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Terraria.DataStructures; 8 | using Terraria.ModLoader.IO; 9 | 10 | namespace StructureHelper.Models.NbtEntries 11 | { 12 | internal class TileEntityNBTEntry : StructureNBTEntry 13 | { 14 | string tileEntityType; 15 | TagCompound tileEntityData; 16 | 17 | public static Func DESERIALIZER = Deserialize; 18 | 19 | public TileEntityNBTEntry(int x, int y, string tileEntityType, TagCompound tileEntityData) : base(x, y) 20 | { 21 | this.tileEntityType = tileEntityType; 22 | this.tileEntityData = tileEntityData; 23 | } 24 | 25 | public static TileEntityNBTEntry Deserialize(TagCompound tag) 26 | { 27 | TileEntityNBTEntry entry = new( 28 | tag.GetInt("x"), 29 | tag.GetInt("y"), 30 | tag.GetString("tileEntityType"), 31 | tag.Get("tileEntityData")); 32 | 33 | return entry; 34 | } 35 | 36 | //TODO: Do we want to move these to their own StructureNBTEntry? 37 | internal void GenerateChest(Point16 pos, TagCompound rules) 38 | { 39 | int i = Chest.CreateChest(pos.X, pos.Y); 40 | 41 | if (i == -1) 42 | return; 43 | 44 | Chest chest = Main.chest[i]; 45 | ChestEntity.SetChest(chest, ChestEntity.LoadChestRules(rules)); 46 | } 47 | 48 | public override void OnGenerate(Point16 generatingAt, bool ignoreNull, GenFlags flags) 49 | { 50 | if (tileEntityType == "StructureHelper/ChestEntity" && !ignoreNull) 51 | { 52 | GenerateChest(generatingAt, tileEntityData); 53 | } 54 | else if ((flags & GenFlags.IgnoreTileEnttiyData) == 0) 55 | { 56 | if (!int.TryParse(tileEntityType, out int typ)) 57 | { 58 | string[] parts = tileEntityType.Split("/", 2); 59 | 60 | if (ModLoader.TryGetMod(parts[0], out Mod mod) && mod.TryFind(parts[1], out ModTileEntity te)) 61 | typ = te.Type; 62 | } 63 | 64 | if (typ != 0) 65 | { 66 | TileEntity.PlaceEntityNet(generatingAt.X, generatingAt.Y, typ); 67 | 68 | if (tileEntityData != null && typ != 2 && TileEntity.ByPosition.ContainsKey(generatingAt)) // We specifically exclude logic sensors (type 2) because these store their absolute pos 69 | TileEntity.ByPosition[generatingAt].LoadData(tileEntityData); 70 | } 71 | } 72 | } 73 | 74 | public override void Serialize(TagCompound tag) 75 | { 76 | tag["tileEntityType"] = tileEntityType; 77 | tag["tileEntityData"] = tileEntityData; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Models/StructureNbtEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Terraria.DataStructures; 7 | using Terraria.ModLoader.IO; 8 | 9 | namespace StructureHelper.Models 10 | { 11 | /// 12 | /// Represents an NBT entry within the NBT section of the structure file. These are used for sparse, 13 | /// complex data within structures like tile entities or chest pools. 14 | /// 15 | /// You MUST implement a DESERIALIZER as per TagSerializable! 16 | /// 17 | public abstract class StructureNBTEntry : TagSerializable 18 | { 19 | /// 20 | /// X coordinate local to the strucure associated with this NBT entry 21 | /// 22 | public int x; 23 | 24 | /// 25 | /// Y coordinate local to the structure associated with this NBT entry 26 | /// 27 | public int y; 28 | 29 | /// 30 | /// Temporary hack deserializer because TML fails to find subtype deserializers 31 | /// 32 | public static readonly Func DESERIALIZER = tag => { 33 | var typeName = tag.GetString(""); 34 | var type = typeof(StructureNBTEntry).Assembly.GetType(typeName) ?? ModLoader.Mods.Select(mod => mod.Code?.GetType(typeName)).Where(t => t != null).FirstOrDefault(); 35 | if (TagSerializer.TryGetSerializer(type, out var serializer)) 36 | return (StructureNBTEntry)serializer.Deserialize(tag); 37 | 38 | throw new ArgumentException($"Missing deserializer for subtype '{typeName}'"); 39 | }; 40 | 41 | /// 42 | /// What needs to happen on structure generation to properly place the object 43 | /// represented by this NBT entry into the world. 44 | /// 45 | public abstract void OnGenerate(Point16 generatingAt, bool ignoreNull, GenFlags flags); 46 | 47 | /// 48 | /// Serialize the custom data for this StructureNbtEntry here 49 | /// 50 | /// The tag to serialize to 51 | public abstract void Serialize(TagCompound tag); 52 | 53 | public StructureNBTEntry(int x, int y) 54 | { 55 | this.x = x; 56 | this.y = y; 57 | } 58 | 59 | public TagCompound SerializeData() 60 | { 61 | TagCompound tag = new() 62 | { 63 | ["x"] = x, 64 | ["y"] = y 65 | }; 66 | 67 | Serialize(tag); 68 | 69 | return tag; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Models/TileDataEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace StructureHelper.Models 9 | { 10 | public unsafe interface ITileDataEntry 11 | { 12 | /// 13 | /// Returns the pointer to the backing data of this TileDataEntry 14 | /// 15 | /// 16 | public void* GetRawPtr(); 17 | 18 | /// 19 | /// Sets the data for this TileDataEntry from a byte array, such as one read from a binary file 20 | /// 21 | /// The raw data to populate this entry with 22 | /// 23 | public void SetData(byte[] data); 24 | 25 | /// 26 | /// Gets the raw data of this tile data entry as a byte array. 27 | /// 28 | /// The raw data as a byte array 29 | public byte[] GetData(); 30 | 31 | /// 32 | /// Gets a single entry at this location in the data. Returns as a void pointer, 33 | /// you will have to derefference this yourself. 34 | /// 35 | /// 36 | /// 37 | /// 38 | public void* GetSingleEntry(int x, int y); 39 | 40 | /// 41 | /// Returns the raw size of this tile data entry, in bytes. 42 | /// 43 | /// The total size of this data entry in bytes 44 | public int GetRawSize(); 45 | 46 | /// 47 | /// Returns the size in bytes of one column of this data 48 | /// 49 | /// 50 | public int GetColumnSize(); 51 | 52 | /// 53 | /// Returns the size in bytes of a single entry of this data 54 | /// 55 | /// 56 | public int GetSingleSize(); 57 | 58 | /// 59 | /// Imports a column of data from the world into this structure 60 | /// 61 | /// The topmost point of the column 62 | /// The index of the column to read into 63 | public void ImportColumn(void* source, int columnIdx); 64 | 65 | /// 66 | /// Copies an entire column of data from this data entry into the world. 67 | /// 68 | /// The pointer to the topmost tiles ITileData of this type to copy into 69 | /// The column of data to place at that location 70 | public void ExportColumn(void* target, int columnIdx); 71 | 72 | /// 73 | /// Copies a single entry of data into the world. 74 | /// 75 | /// The pointer to the tiles ITileData of this type to copy into 76 | /// The X position to copy from 77 | /// The Y position to copy from 78 | public void ExportSingle(void* target, int columnIdx, int rowIdx); 79 | } 80 | 81 | public unsafe class TileDataEntry : ITileDataEntry, IDisposable where T : unmanaged, ITileData 82 | { 83 | private readonly T[] rawData; 84 | private readonly T* rawDataPtr; 85 | 86 | private GCHandle rawDataHandle; 87 | 88 | public int length; 89 | public int colLength; 90 | 91 | public TileDataEntry(int length, int rowLength) 92 | { 93 | rawData = new T[length]; 94 | rawDataHandle = GCHandle.Alloc(rawData, GCHandleType.Pinned); 95 | rawDataPtr = (T*)rawDataHandle.AddrOfPinnedObject().ToPointer(); 96 | 97 | this.length = length; 98 | this.colLength = rowLength; 99 | } 100 | 101 | public void* GetRawPtr() 102 | { 103 | return rawDataPtr; 104 | } 105 | 106 | public void SetData(byte[] data) 107 | { 108 | if (data.Length != sizeof(T) * length) 109 | throw new ArgumentException($"Data size is inappropriate, Expected enough for {length} {typeof(T).Name} ({sizeof(T) * length} bytes), but got {data.Length}"); 110 | 111 | fixed (byte* dataPtr = data) 112 | { 113 | Buffer.MemoryCopy(dataPtr, rawDataPtr, data.Length, data.Length); 114 | } 115 | } 116 | 117 | public byte[] GetData() 118 | { 119 | byte[] data = new byte[GetRawSize()]; 120 | 121 | fixed (byte* dataPtr = data) 122 | { 123 | Buffer.MemoryCopy(rawDataPtr, dataPtr, data.Length, data.Length); 124 | } 125 | 126 | return data; 127 | } 128 | 129 | public void* GetSingleEntry(int x, int y) 130 | { 131 | return rawDataPtr + (x * colLength + y); 132 | } 133 | 134 | public int GetRawSize() 135 | { 136 | return sizeof(T) * length; 137 | } 138 | 139 | public int GetColumnSize() 140 | { 141 | return sizeof(T) * colLength; 142 | } 143 | 144 | public int GetSingleSize() 145 | { 146 | return sizeof(T); 147 | } 148 | 149 | public void ImportColumn(void* source, int colIdx) 150 | { 151 | long colSize = GetColumnSize(); 152 | 153 | T* target = rawDataPtr + colIdx * colLength; 154 | Buffer.MemoryCopy(source, target, colSize, colSize); 155 | } 156 | 157 | public void ExportColumn(void* target, int colIdx) 158 | { 159 | long colSize = GetColumnSize(); 160 | 161 | T* source = rawDataPtr + colIdx * colLength; 162 | Buffer.MemoryCopy(source, target, colSize, colSize); 163 | } 164 | 165 | public void ExportSingle(void* target, int colIdx, int rowIdx) 166 | { 167 | T* source = rawDataPtr + (colIdx * colLength + rowIdx); 168 | Buffer.MemoryCopy(source, target, GetSingleSize(), GetSingleSize()); 169 | } 170 | 171 | public void Dispose() 172 | { 173 | rawDataHandle.Free(); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Porting/Porter.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.API; 2 | using StructureHelper.Content.GUI; 3 | using StructureHelper.Models; 4 | using StructureHelper.Models.NbtEntries; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Terraria; 12 | using Terraria.DataStructures; 13 | using Terraria.ID; 14 | using Terraria.ModLoader; 15 | using Terraria.ModLoader.IO; 16 | using static System.Net.WebRequestMethods; 17 | 18 | namespace StructureHelper.NewFolder 19 | { 20 | internal class Porter 21 | { 22 | public static void AddDataEntry(StructureData target, Mod mod) where T:unmanaged, ITileData 23 | { 24 | string key = $"{mod?.Name ?? "Terraria"}/{typeof(T).Name}"; 25 | 26 | if (!target.dataEntries.ContainsKey(key)) 27 | target.dataEntries.Add(key, new TileDataEntry(target.width * target.height, target.height)); 28 | } 29 | 30 | public static unsafe void InsertDataEntry(StructureData target, Mod mod, int x, int y, void* value) where T : unmanaged, ITileData 31 | { 32 | string key = $"{mod?.Name ?? "Terraria"}/{typeof(T).Name}"; 33 | 34 | void* memTarget = target.dataEntries[key].GetSingleEntry(x, y); 35 | Buffer.MemoryCopy(value, memTarget, sizeof(T), sizeof(T)); 36 | } 37 | 38 | public static unsafe StructureData PortStructure(TagCompound tag) 39 | { 40 | StructureData ported = new StructureData(); 41 | 42 | var data = (List)tag.GetList("TileData"); 43 | 44 | int width = tag.GetInt("Width"); 45 | int height = tag.GetInt("Height"); 46 | 47 | ported.width = width + 1; 48 | ported.height = height + 1; 49 | ported.version = StructureHelper.Instance.Version; 50 | 51 | AddDataEntry(ported, null); 52 | AddDataEntry(ported, null); 53 | AddDataEntry(ported, null); 54 | AddDataEntry(ported, null); 55 | AddDataEntry(ported, null); 56 | 57 | for (int x = 0; x <= width; x++) 58 | { 59 | bool isSlow = false; 60 | 61 | for (int y = 0; y <= height; y++) 62 | { 63 | int index = y + x * (height + 1); 64 | TileSaveData d = data[index]; 65 | 66 | bool isNullTile = false; 67 | bool isNullWall = false; 68 | 69 | if (!ushort.TryParse(d.tile, out ushort type)) 70 | { 71 | string[] parts = d.tile.Split(); 72 | 73 | if (parts[0] == "StructureHelper" && parts[1] == "NullBlock") 74 | { 75 | ported.moddedTileTable.TryAdd(StructureHelper.NullTileID, StructureHelper.NULL_IDENTIFIER); 76 | type = StructureHelper.NULL_IDENTIFIER; 77 | isSlow = true; 78 | } 79 | else if (parts.Length > 1 && ModLoader.TryGetMod(parts[0], out Mod mod) && mod.TryFind(parts[1], out ModTile modTileType)) 80 | { 81 | ported.moddedTileTable.TryAdd(modTileType.Type, modTileType.Type); 82 | type = modTileType.Type; 83 | } 84 | else 85 | { 86 | type = 0; 87 | } 88 | } 89 | 90 | if (!ushort.TryParse(d.wall, out ushort wallType)) 91 | { 92 | string[] parts = d.wall.Split(); 93 | 94 | if (parts[0] == "StructureHelper" && parts[1] == "NullWall") 95 | { 96 | ported.moddedWallTable.TryAdd(StructureHelper.NullWallID, StructureHelper.NULL_IDENTIFIER); 97 | wallType = StructureHelper.NULL_IDENTIFIER; 98 | isSlow = true; 99 | } 100 | else if (parts.Length > 1 && ModLoader.TryGetMod(parts[0], out Mod mod) && mod.TryFind(parts[1], out ModWall modWallType)) 101 | { 102 | ported.moddedWallTable.TryAdd(modWallType.Type, modWallType.Type); 103 | wallType = modWallType.Type; 104 | } 105 | else 106 | { 107 | wallType = 0; 108 | } 109 | } 110 | 111 | if (!d.Active) 112 | isNullTile = false; 113 | 114 | InsertDataEntry(ported, null, x, y, &type); 115 | InsertDataEntry(ported, null, x, y, &wallType); 116 | InsertDataEntry(ported, null, x, y, &d.packedLiquidData); 117 | InsertDataEntry(ported, null, x, y, &d.brightInvisibleData); 118 | 119 | var reconstructed = new TileWallWireStateData(); 120 | reconstructed.TileFrameX = d.frameX; 121 | reconstructed.TileFrameY = d.frameY; 122 | 123 | void* ptr = &reconstructed; 124 | 125 | int* intPtr = (int*)ptr; 126 | intPtr++; 127 | 128 | *intPtr = d.wallWireData; 129 | 130 | if (!d.Active) 131 | reconstructed.HasTile = false; 132 | 133 | InsertDataEntry(ported, null, x, y, &reconstructed); 134 | 135 | 136 | if (d.TEType != "") //place and load a tile entity 137 | { 138 | 139 | if (!int.TryParse(d.TEType, out int typ)) 140 | { 141 | string[] parts = d.TEType.Split(); 142 | 143 | if (ModLoader.TryGetMod(parts[0], out Mod mod) && mod.TryFind(parts[1], out ModTileEntity te)) 144 | typ = te.Type; 145 | } 146 | 147 | if (typ != 0) 148 | { 149 | var entity = TileEntity.manager.GenerateInstance(typ); 150 | 151 | var modTileEntity = entity as ModTileEntity; 152 | string teName; 153 | 154 | if (modTileEntity != null) 155 | teName = modTileEntity.FullName; 156 | else 157 | teName = entity.type.ToString(); 158 | 159 | ported.containsNbt = true; 160 | ported.nbtData ??= new(); 161 | 162 | ported.nbtData.Add(new TileEntityNBTEntry(x, y, teName, d.TEData)); 163 | } 164 | } 165 | } 166 | ported.slowColumns.Add(x, isSlow); 167 | } 168 | 169 | return ported; 170 | } 171 | 172 | public static MultiStructureData PortMultiStructure(TagCompound tag) 173 | { 174 | MultiStructureData ported = new MultiStructureData(); 175 | 176 | var oldStructures = (List)tag.GetList("Structures"); 177 | 178 | ported.count = oldStructures.Count; 179 | 180 | for(int k = 0; k < oldStructures.Count; k++) 181 | { 182 | ported.structures.Add(PortStructure(oldStructures[k])); 183 | } 184 | 185 | return ported; 186 | } 187 | 188 | public static void PortFile(string path) 189 | { 190 | var tag = API.Legacy.LegacyGenerator.GetTag(path, StructureHelper.Instance, true); 191 | var data = PortStructure(tag); 192 | 193 | Saver.SaveToFile(data, path); 194 | } 195 | 196 | public static void PortMultiFile(string path) 197 | { 198 | var tag = API.Legacy.LegacyGenerator.GetTag(path, StructureHelper.Instance, true); 199 | var data = PortMultiStructure(tag); 200 | 201 | Saver.SaveMultistructureToFile(data, path); 202 | } 203 | 204 | public static void PortDirectory(string dir) 205 | { 206 | string[] filePaths = Directory.GetFiles(dir); 207 | 208 | foreach (string path in filePaths) 209 | { 210 | string name = Path.GetFileName(path); 211 | 212 | if (!Path.HasExtension(path)) 213 | { 214 | try 215 | { 216 | if (API.Legacy.LegacyGenerator.GetTag(path, StructureHelper.Instance, true).ContainsKey("Structures")) 217 | PortMultiFile(path); 218 | else 219 | PortFile(path); 220 | } 221 | catch 222 | { 223 | Main.NewText($"A file in your SavedStructures directory didnt seem to be a structure ({name}), it will be skipped!"); 224 | continue; 225 | } 226 | } 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Terraria": { 4 | "commandName": "Executable", 5 | "executablePath": "dotnet", 6 | "commandLineArgs": "$(tMLPath)", 7 | "workingDirectory": "$(tMLSteamPath)" 8 | }, 9 | "TerrariaServer": { 10 | "commandName": "Executable", 11 | "executablePath": "dotnet", 12 | "commandLineArgs": "$(tMLServerPath)", 13 | "workingDirectory": "$(tMLSteamPath)" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /SampleChest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/SampleChest -------------------------------------------------------------------------------- /StructureHelper.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.Xna.Framework; 2 | global using Microsoft.Xna.Framework.Graphics; 3 | global using Terraria; 4 | global using Terraria.ModLoader; 5 | using StructureHelper.Content.Tiles; 6 | using System.Reflection.Metadata; 7 | 8 | namespace StructureHelper 9 | { 10 | public class StructureHelper : Mod 11 | { 12 | public static ushort NullTileID; 13 | public static ushort NullWallID; 14 | 15 | public const ushort NULL_IDENTIFIER = ushort.MaxValue; 16 | 17 | public StructureHelper() { Instance = this; } 18 | 19 | public static StructureHelper Instance { get; set; } 20 | 21 | public override void PostSetupContent() 22 | { 23 | NullTileID = (ushort)ModContent.TileType(); 24 | NullWallID = (ushort)ModContent.WallType(); 25 | } 26 | 27 | public override void Unload() 28 | { 29 | API.Legacy.LegacyGenerator.StructureDataCache.Clear(); 30 | API.Generator.StructureCache.Clear(); 31 | API.MultiStructureGenerator.MultiStructureCache.Clear(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /StructureHelper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | StructureHelper 6 | latest 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /StructureHelper.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29123.88 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructureHelper", "StructureHelper.csproj", "{E8EC1BE9-A321-47D3-A40E-DD5D51D973A1}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {E8EC1BE9-A321-47D3-A40E-DD5D51D973A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {E8EC1BE9-A321-47D3-A40E-DD5D51D973A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {E8EC1BE9-A321-47D3-A40E-DD5D51D973A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {E8EC1BE9-A321-47D3-A40E-DD5D51D973A1}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {890D3A7C-E2A6-45A2-A67D-6437D09F9C36} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Util/LegacyStructurePreview.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Terraria.ModLoader.IO; 4 | 5 | namespace StructureHelper.Util 6 | { 7 | /// 8 | /// Container for a structure's preview image. 9 | /// 10 | public class LegacyStructurePreview : IDisposable 11 | { 12 | private readonly TagCompound tag; 13 | 14 | /// 15 | /// The name of the structure this is previewing 16 | /// 17 | public readonly string name; 18 | 19 | /// 20 | /// The actual texture of the preview 21 | /// 22 | public Texture2D preview; 23 | 24 | /// 25 | /// Width of the preview texture, in pixels 26 | /// 27 | public int Width => preview?.Width ?? 1; 28 | 29 | /// 30 | /// Height of the preview texture, in pixels 31 | /// 32 | public int Height => preview?.Height ?? 1; 33 | 34 | public LegacyStructurePreview(string name, TagCompound structure) 35 | { 36 | this.name = name; 37 | tag = structure; 38 | 39 | LegacyPreviewRenderQueue.queue.Add(this); 40 | } 41 | 42 | /// 43 | /// Renders and saves the actual preview to a RenderTarget. 44 | /// 45 | /// The texture created 46 | internal Texture2D GeneratePreview() 47 | { 48 | int width = tag.GetInt("Width"); 49 | int height = tag.GetInt("Height"); 50 | 51 | var data = (List)tag.GetList("TileData"); 52 | 53 | RenderTargetBinding[] oldTargets = Main.graphics.GraphicsDevice.GetRenderTargets(); 54 | 55 | RenderTarget2D newTexture = new(Main.graphics.GraphicsDevice, (width + 1) * 16, (height + 1) * 16, false, default, default, default, RenderTargetUsage.PreserveContents); 56 | 57 | Main.graphics.GraphicsDevice.SetRenderTarget(newTexture); 58 | 59 | Main.graphics.GraphicsDevice.Clear(Color.Transparent); 60 | 61 | Main.spriteBatch.Begin(); 62 | 63 | for (int x = 0; x <= width; x++) 64 | { 65 | for (int y = 0; y <= height; y++) 66 | { 67 | int index = y + x * (height + 1); 68 | TileSaveData d = data[index]; 69 | 70 | if (!int.TryParse(d.tile, out int type)) 71 | { 72 | string[] parts = d.tile.Split(); 73 | 74 | if (parts.Length > 1 && ModLoader.TryGetMod(parts[0], out Mod mod) && mod.TryFind(parts[1], out ModTile modTileType)) 75 | type = modTileType.Type; 76 | else 77 | type = 0; 78 | } 79 | 80 | if (!int.TryParse(d.wall, out int wallType)) 81 | { 82 | string[] parts = d.wall.Split(); 83 | 84 | if (parts.Length > 1 && ModLoader.TryGetMod(parts[0], out Mod mod) && mod.TryFind(parts[1], out ModWall modWallType)) 85 | wallType = modWallType.Type; 86 | else 87 | wallType = 0; 88 | } 89 | 90 | if (wallType != 0 && d.wall != "StructureHelper NullWall") 91 | { 92 | Texture2D tex = Terraria.GameContent.TextureAssets.Wall[wallType].Value; 93 | Main.spriteBatch.Draw(tex, new Rectangle(x * 16, y * 16, 16, 16), new Rectangle(8, 8, 16, 16), Color.White); 94 | } 95 | 96 | if (d.Active && d.tile != "StructureHelper NullBlock") 97 | { 98 | Texture2D tex = Terraria.GameContent.TextureAssets.Tile[type].Value; 99 | Main.spriteBatch.Draw(tex, new Rectangle(x * 16, y * 16, 16, 16), new Rectangle(d.frameX, d.frameY, 16, 16), Color.White); 100 | } 101 | } 102 | } 103 | 104 | Main.spriteBatch.End(); 105 | 106 | Main.graphics.GraphicsDevice.SetRenderTargets(null); 107 | Main.graphics.GraphicsDevice.SetRenderTargets(oldTargets); 108 | return newTexture; 109 | } 110 | 111 | public void Dispose() 112 | { 113 | preview?.Dispose(); 114 | } 115 | } 116 | 117 | public class LegacyPreviewRenderQueue : ILoadable 118 | { 119 | /// 120 | /// A queue of previews to render textures for when the next opportunity arises 121 | /// 122 | public static List queue = []; 123 | 124 | public void Load(Mod mod) 125 | { 126 | On_Main.CheckMonoliths += DrawQueuedPreview; 127 | } 128 | 129 | public void Unload() 130 | { 131 | 132 | } 133 | 134 | /// 135 | /// When the opportunity in the rendering cycle arises, render out all of the queued previews 136 | /// 137 | /// 138 | private void DrawQueuedPreview(On_Main.orig_CheckMonoliths orig) 139 | { 140 | foreach (LegacyStructurePreview queued in queue) 141 | { 142 | queued.preview = queued.GeneratePreview(); 143 | } 144 | 145 | queue.Clear(); 146 | 147 | orig(); 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /Util/StructurePreview.cs: -------------------------------------------------------------------------------- 1 | using StructureHelper.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace StructureHelper.Util 5 | { 6 | public class StructurePreview 7 | { 8 | public StructureData data; 9 | 10 | /// 11 | /// The name of the structure this is previewing 12 | /// 13 | public readonly string name; 14 | 15 | /// 16 | /// The actual texture of the preview 17 | /// 18 | public Texture2D preview; 19 | 20 | /// 21 | /// Width of the preview texture, in pixels 22 | /// 23 | public int Width => preview?.Width ?? 1; 24 | 25 | /// 26 | /// Height of the preview texture, in pixels 27 | /// 28 | public int Height => preview?.Height ?? 1; 29 | 30 | public StructurePreview(string name, StructureData data) 31 | { 32 | this.name = name; 33 | this.data = data; 34 | 35 | PreviewRenderQueue.queue.Add(this); 36 | } 37 | 38 | /// 39 | /// Renders and saves the actual preview to a RenderTarget. 40 | /// 41 | /// The texture created 42 | internal unsafe Texture2D GeneratePreview() 43 | { 44 | int width = data.width; 45 | int height = data.height; 46 | 47 | RenderTargetBinding[] oldTargets = Main.graphics.GraphicsDevice.GetRenderTargets(); 48 | 49 | RenderTarget2D newTexture = new(Main.graphics.GraphicsDevice, (width) * 16, (height) * 16, false, default, default, default, RenderTargetUsage.PreserveContents); 50 | 51 | Main.graphics.GraphicsDevice.SetRenderTarget(newTexture); 52 | 53 | Main.graphics.GraphicsDevice.Clear(Color.Transparent); 54 | 55 | Main.spriteBatch.Begin(); 56 | 57 | for (int x = 0; x < width; x++) 58 | { 59 | for (int y = 0; y < height; y++) 60 | { 61 | TileWallWireStateData wireStateData = *(TileWallWireStateData*)data.dataEntries["Terraria/TileWallWireStateData"].GetSingleEntry(x, y); 62 | ushort tileType = (*(TileTypeData*)data.dataEntries["Terraria/TileTypeData"].GetSingleEntry(x, y)).Type; 63 | ushort wallType = (*(WallTypeData*)data.dataEntries["Terraria/WallTypeData"].GetSingleEntry(x, y)).Type; 64 | int frameX = wireStateData.TileFrameX; 65 | int frameY = wireStateData.TileFrameY; 66 | bool hasTile = wireStateData.HasTile; 67 | 68 | if (wallType != 0 && wallType != StructureHelper.NULL_IDENTIFIER && wallType < Terraria.GameContent.TextureAssets.Wall.Length) 69 | { 70 | Texture2D tex = Terraria.GameContent.TextureAssets.Wall[wallType].Value; 71 | Main.spriteBatch.Draw(tex, new Rectangle(x * 16, y * 16, 16, 16), new Rectangle(8, 8, 16, 16), Color.White); 72 | } 73 | 74 | if (hasTile && tileType != StructureHelper.NULL_IDENTIFIER && tileType < Terraria.GameContent.TextureAssets.Tile.Length) 75 | { 76 | Texture2D tex = Terraria.GameContent.TextureAssets.Tile[tileType].Value; 77 | Main.spriteBatch.Draw(tex, new Rectangle(x * 16, y * 16, 16, 16), new Rectangle(frameX, frameY, 16, 16), Color.White); 78 | } 79 | } 80 | } 81 | 82 | Main.spriteBatch.End(); 83 | 84 | Main.graphics.GraphicsDevice.SetRenderTargets(null); 85 | Main.graphics.GraphicsDevice.SetRenderTargets(oldTargets); 86 | return newTexture; 87 | } 88 | 89 | public void Dispose() 90 | { 91 | preview?.Dispose(); 92 | } 93 | } 94 | 95 | public class PreviewRenderQueue : ILoadable 96 | { 97 | /// 98 | /// A queue of previews to render textures for when the next opportunity arises 99 | /// 100 | public static List queue = []; 101 | 102 | public void Load(Mod mod) 103 | { 104 | On_Main.CheckMonoliths += DrawQueuedPreview; 105 | } 106 | 107 | public void Unload() 108 | { 109 | 110 | } 111 | 112 | /// 113 | /// When the opportunity in the rendering cycle arises, render out all of the queued previews 114 | /// 115 | /// 116 | private void DrawQueuedPreview(On_Main.orig_CheckMonoliths orig) 117 | { 118 | foreach (StructurePreview queued in queue) 119 | { 120 | queued.preview = queued.GeneratePreview(); 121 | } 122 | 123 | queue.Clear(); 124 | 125 | orig(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /WikiPics/Buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/WikiPics/Buttons.png -------------------------------------------------------------------------------- /WikiPics/Weight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/WikiPics/Weight.png -------------------------------------------------------------------------------- /WikiPics/noWeight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/WikiPics/noWeight.png -------------------------------------------------------------------------------- /build.txt: -------------------------------------------------------------------------------- 1 | displayName = Structure Helper 2 | author = ScalarVector 3 | version = 3.0.1 4 | homepage = https://github.com/ScalarVector1/StructureHelper/wiki -------------------------------------------------------------------------------- /description.txt: -------------------------------------------------------------------------------- 1 | The famous world generation library, now better, faster, and more efficient than ever! 2 | 3 | StructureHelper 3.0 has arrived, with slimmer files, faster generation, and an easier to use API! You can find porting instructions here if you're a developer: https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide 4 | 5 | [[[About]]] 6 | 7 | StructureHelper is a simple tool which allows mod creators to build world generation structures in-game and save them to external files, as opposed to manually encoding them into their source code (usually to the detriment of readability and maintainability). It allows both programmer and non-programmer mod creators to easily build complex world generation using all of the games features, like paint, wiring, tile entities, and liquid. Once structures are saved, they're easy to share, swap out, and keep track of in source control like any other asset. The mod can also be used to easily copy and move sections of the world around during the game! 8 | 9 | [[[Links]]] 10 | 11 | For developers looking to use the mod, check out the documentation here: 12 | - https://github.com/ScalarVector1/StructureHelper/wiki 13 | 14 | For developers or players looking for support, join my discord server: 15 | - https://discord.gg/KFpCz5p688 16 | 17 | For those that want to support me, you can do so on ko-fi: 18 | - https://ko-fi.com/scalarvector 19 | 20 | [[[Credits and Special Thanks]]] 21 | 22 | https://steamcommunity.com/id/Mirsario Who is responsible for the tile reworks in TML enabling some of the changes I made to speed things up, and for giving some great suggestions on how the generation can be optimized 23 | (Check out their mod! https://steamcommunity.com/sharedfiles/filedetails/?id=2811803870) 24 | 25 | https://steamcommunity.com/id/steviegt6 Who was a big help with working out kinks and optimizing the new format and generation 26 | (Check out their mods! https://steamcommunity.com/id/steviegt6/myworkshopfiles/) 27 | 28 | https://steamcommunity.com/id/dylandoe Who was on the front line testing the update and calling out like 7 different major issues before they hit the public 29 | (Check out their mod! https://steamcommunity.com/sharedfiles/filedetails/?id=2833852432) 30 | 31 | https://steamcommunity.com/profiles/76561198191753703 Who was also on the front line testing the update and calling out my stupidity 32 | (Check out their mod! https://steamcommunity.com/sharedfiles/filedetails/?id=2871665851) 33 | -------------------------------------------------------------------------------- /description_workshop.txt: -------------------------------------------------------------------------------- 1 | [h1]The famous world generation library, now better, faster, and more efficient than ever![/h1] 2 | 3 | StructureHelper 3.0 has arrived, with slimmer files, faster generation, and an easier to use API! You can find porting instructions here if you're a developer: https://github.com/ScalarVector1/StructureHelper/wiki/3.0-Porting-Guide 4 | 5 | [h1]About[/h1] 6 | 7 | StructureHelper is a simple tool which allows mod creators to build world generation structures in-game and save them to external files, as opposed to manually encoding them into their source code (usually to the detriment of readability and maintainability). It allows both programmer and non-programmer mod creators to easily build complex world generation using all of the games features, like paint, wiring, tile entities, and liquid. Once structures are saved, they're easy to share, swap out, and keep track of in source control like any other asset. The mod can also be used to easily copy and move sections of the world around during the game! 8 | 9 | [h1]Links[/h1] 10 | 11 | For developers looking to use the mod, check out the documentation here: 12 | - https://github.com/ScalarVector1/StructureHelper/wiki 13 | 14 | For developers or players looking for support, join my discord server: 15 | - https://discord.gg/KFpCz5p688 16 | 17 | For those that want to support me, you can do so on ko-fi: 18 | - https://ko-fi.com/scalarvector 19 | 20 | [h1]Credits and Special Thanks[/h1] 21 | 22 | https://steamcommunity.com/id/Mirsario Who is responsible for the tile reworks in TML enabling some of the changes I made to speed things up, and for giving some great suggestions on how the generation can be optimized 23 | (Check out their mod! https://steamcommunity.com/sharedfiles/filedetails/?id=2811803870) 24 | 25 | https://steamcommunity.com/id/steviegt6 Who was a big help with working out kinks and optimizing the new format and generation 26 | (Check out their mods! https://steamcommunity.com/id/steviegt6/myworkshopfiles/) 27 | 28 | https://steamcommunity.com/id/dylandoe Who was on the front line testing the update and calling out like 7 different major issues before they hit the public 29 | (Check out their mod! https://steamcommunity.com/sharedfiles/filedetails/?id=2833852432) 30 | 31 | https://steamcommunity.com/profiles/76561198191753703 Who was also on the front line testing the update and calling out my stupidity 32 | (Check out their mod! https://steamcommunity.com/sharedfiles/filedetails/?id=2871665851) 33 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/icon.png -------------------------------------------------------------------------------- /icon_workshop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScalarVector1/StructureHelper/bd821448865b07bf687e43c4e7c2594636464178/icon_workshop.png -------------------------------------------------------------------------------- /workshop.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkshopPublishedVersion": 1, 3 | "ContentType": "Mod", 4 | "SteamEntryId": 2790924965, 5 | "Publicity": 2 6 | } --------------------------------------------------------------------------------