├── .gitignore ├── Api ├── Backups.cs ├── Config.cs ├── Modpacks.cs ├── MyMods.cs └── Utilities │ ├── AssemblyPatching.cs │ ├── FolderSelectDialog.cs │ ├── TransparentPictureBox.cs │ └── Utility.cs ├── App.config ├── Form2.Designer.cs ├── Form2.cs ├── LICENSE ├── MCC Mod Manager.csproj ├── MCC Mod Manager.sln ├── MasterForm.Designer.cs ├── MasterForm.cs ├── MasterForm.resx ├── ModManagerCustomControls ├── CustomTabControl.cs ├── ModManagerCustomControls.csproj └── Properties │ └── AssemblyInfo.cs ├── ModpackRenameForm.Designer.cs ├── ModpackRenameForm.cs ├── ModpackRenameForm.resx ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings ├── README.md ├── Resources ├── HaloHelmetIcon.ico ├── HaloHelmetIcon_small.png ├── caution_15px.png ├── caution_35px.png ├── greenDot_15px.png └── redDot_15px.png └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | 5 | # Dependencies should be pulled down separately from source <3 6 | packages/ 7 | FodyWeavers.xml 8 | FodyWeavers.xsd -------------------------------------------------------------------------------- /Api/Backups.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | using Newtonsoft.Json; 7 | using System.Windows.Forms; 8 | using System.Drawing; 9 | using System.IO.Compression; 10 | using MCC_Mod_Manager.Api.Utilities; 11 | 12 | namespace MCC_Mod_Manager.Api { 13 | static class Backups { 14 | public static Dictionary _baks = new Dictionary(); 15 | 16 | #region Event Handlers 17 | 18 | public static void ShowFullPathCheckbox_Click(object sender, EventArgs e) { 19 | Config.fullBakPath = Program.MasterForm.fullBakPath_chb.Checked; 20 | foreach (Panel p in Program.MasterForm.bakListPanel.Controls.OfType()) { 21 | CheckBox chb = (CheckBox)p.GetChildAtPoint(Config.BackupsChbPoint); 22 | if (Config.fullBakPath) { // swapping from filename to full path 23 | chb.Text = Config.dirtyPadding + Backups.GetBakKey(chb.Text.Replace(Config.dirtyPadding, "")); 24 | p.Width = chb.Width + 40; 25 | } else { // swapping from full path to filename 26 | chb.Text = Config.dirtyPadding + Backups._baks[chb.Text.Replace(Config.dirtyPadding, "")]; 27 | p.Width = Config.backupPanelWidth; 28 | } 29 | } 30 | } 31 | 32 | public static void MakeBakBtn_Click(object sender, EventArgs e) { 33 | EnsureBackupFolderExists(); 34 | 35 | OpenFileDialog ofd = new OpenFileDialog { 36 | InitialDirectory = Config.MCC_home, 37 | Multiselect = true 38 | }; 39 | 40 | bool newbaks = false; 41 | if (ofd.ShowDialog() == DialogResult.OK) { 42 | foreach (string file in ofd.FileNames) { 43 | if (_baks.ContainsKey(file)) { 44 | DialogResult ans = Utility.ShowMsg("A backup of ' " + file + "' already exists. Would you like to overwrite?", "Question"); 45 | if (ans == DialogResult.No) { 46 | continue; 47 | } 48 | } 49 | 50 | if (CreateBackup(file, true) != 0) { 51 | Utility.ShowMsg("Could not create a backup of '" + file + "'. Is the file open somewhere?", "Error"); 52 | } else { 53 | newbaks = true; 54 | } 55 | } 56 | 57 | if (newbaks) { 58 | Utility.ShowMsg("New Backup(s) Created", "Info"); 59 | SaveBackups(); 60 | LoadBackups(); 61 | } 62 | } 63 | } 64 | 65 | private static Dictionary> checkForPartialRestores(List paths) { 66 | // paths is list of full, original file paths of backed up files 67 | Dictionary> enabledModpacks = new Dictionary>(); 68 | foreach (KeyValuePair item in Config.Patched) { 69 | enabledModpacks[item.Key] = new List(item.Value.files.Keys); 70 | } 71 | 72 | foreach (string path in paths) { 73 | foreach (KeyValuePair> modpack in enabledModpacks) { 74 | if (modpack.Value.Contains(Utility.CompressPath(path))) { 75 | modpack.Value.Remove(Utility.CompressPath(path)); 76 | break; 77 | } 78 | } 79 | } 80 | 81 | Dictionary> results = new Dictionary>(); 82 | results["disable"] = new List(); 83 | results["partials"] = new List(); 84 | 85 | Dictionary enabledModpacksCheck = Config.Patched; 86 | foreach (KeyValuePair> modpack in enabledModpacks) { 87 | if (!modpack.Value.Any()) { 88 | results["disable"].Add(modpack.Key); 89 | } else if (modpack.Value.Count != enabledModpacksCheck[modpack.Key].files.Count) { 90 | results["partials"].Add(modpack.Key); 91 | } 92 | } 93 | 94 | return results; 95 | } 96 | 97 | public static void RestoreSelectedBtn_Click(object sender, EventArgs e) { 98 | IEnumerable bakList = Program.MasterForm.bakListPanel.Controls.OfType(); 99 | List backupPaths = new List(); 100 | foreach (Panel p in bakList) { 101 | CheckBox chb = (CheckBox)p.GetChildAtPoint(Config.BackupsChbPoint); 102 | if (chb.Checked) { 103 | if (Program.MasterForm.fullBakPath_chb.Checked) { 104 | backupPaths.Add(chb.Text.Replace(Config.dirtyPadding, "")); 105 | } else { 106 | backupPaths.Add(GetBakKey(chb.Text.Replace(Config.dirtyPadding, ""))); 107 | } 108 | chb.Checked = false; 109 | } 110 | } 111 | 112 | if (!backupPaths.Any()) { 113 | Utility.ShowMsg("No items selected from the list.", "Error"); 114 | return; 115 | } 116 | Dictionary> x = checkForPartialRestores(backupPaths); 117 | if (x["partials"].Any()) { 118 | DialogResult ans = Utility.ShowMsg("Restoring the selected backups will partially unpatch at least one modpack. All files within a modpack " + 119 | "are intended to be patched and unpatched together. This may cause issues with your game.\r\nContinue?", "Question"); 120 | if (ans == DialogResult.No) { 121 | return; 122 | } 123 | } 124 | int r = RestoreBaks(backupPaths); 125 | MyMods.setModpacksDisabled(x["disable"]); 126 | MyMods.setModpacksPartial(x["partials"]); 127 | 128 | if (r == 0) { 129 | Utility.ShowMsg("Selected files have been restored.", "Info"); 130 | } else if (r == 1 || r == 2) { 131 | Utility.ShowMsg("At least one file restore failed. Your game may be in an unstable state.", "Warning"); 132 | } 133 | } 134 | 135 | public static void RestoreAllBaksBtn_Click(object sender, EventArgs e) { 136 | EnsureBackupFolderExists(); 137 | List bakList = new List(); 138 | foreach (KeyValuePair b in _baks) { 139 | bakList.Add(b.Key); 140 | } 141 | 142 | Dictionary> x = checkForPartialRestores(bakList); 143 | if (x["partials"].Any()) { 144 | DialogResult ans = Utility.ShowMsg("Restoring the selected backups will partially unpatch at least one modpack. All files within a modpack " + 145 | "are intended to be patched and unpatched together. This may cause issues with your game.\r\nContinue?", "Question"); 146 | if (ans == DialogResult.No) { 147 | return; 148 | } 149 | } 150 | int r = RestoreBaks(bakList); 151 | MyMods.setModpacksDisabled(x["disable"]); 152 | MyMods.setModpacksPartial(x["partials"]); 153 | 154 | if (r == 0) { 155 | Utility.ShowMsg("Files have been restored.", "Info"); 156 | } else if (r == 1 || r == 2) { 157 | Utility.ShowMsg("At least one file restore failed. Your game may be in an unstable state.", "Warning"); 158 | } 159 | } 160 | 161 | public static void DelSelectedBak_Click(object sender, EventArgs e) { 162 | IEnumerable bakList = Program.MasterForm.bakListPanel.Controls.OfType(); 163 | EnsureBackupFolderExists(); 164 | 165 | DialogResult ans = Utility.ShowMsg("Are you sure you want to delete the selected backup(s)?\r\nNo crying afterwards?", "Question"); 166 | if (ans == DialogResult.No) { 167 | return; 168 | } 169 | 170 | bool chk = false; 171 | Program.MasterForm.PBar_show(bakList.Count()); 172 | List toDelete = new List(); 173 | foreach (Panel p in bakList) { 174 | CheckBox chb = (CheckBox)p.GetChildAtPoint(Config.BackupsChbPoint); 175 | Program.MasterForm.PBar_update(); 176 | if (chb.Checked) { 177 | chk = true; 178 | string path; 179 | if (Program.MasterForm.fullBakPath_chb.Checked) { 180 | path = chb.Text.Replace(Config.dirtyPadding, ""); 181 | } else { 182 | path = GetBakKey(chb.Text.Replace(Config.dirtyPadding, "")); 183 | } 184 | toDelete.Add(path); 185 | chb.Checked = false; 186 | } 187 | } 188 | if (chk) { 189 | List requiredBaks = FilterNeededBackups(toDelete); 190 | foreach (string path in toDelete) { 191 | if (requiredBaks.Contains(path)) { 192 | continue; 193 | } 194 | if (Utility.DeleteFile(Config.Backup_dir + @"\" + _baks[path])) { 195 | _baks.Remove(path); 196 | } else { 197 | Utility.ShowMsg("Could not delete '" + _baks[path] + "'. Is the file open somewhere?", "Error"); 198 | } 199 | } 200 | 201 | if (requiredBaks.Count == 0) { 202 | Utility.ShowMsg("Selected files have been deleted.", "Info"); 203 | } else { 204 | Utility.ShowMsg(requiredBaks.Count + " backup(s) were not deleted because the original file(s) are currently patched with a mod. " + 205 | "Deleting these backups would make it impossible to unpatch the mod(s).", "Info"); 206 | } 207 | SaveBackups(); 208 | UpdateBackupList(); 209 | Program.MasterForm.PBar_hide(); 210 | } else { 211 | Utility.ShowMsg("No items selected from the list.", "Error"); 212 | Program.MasterForm.PBar_hide(); 213 | return; 214 | } 215 | } 216 | 217 | public static void DelAllBaksBtn_Click(object sender, EventArgs e) { 218 | Backups.DeleteAll(false); 219 | } 220 | 221 | #endregion 222 | 223 | #region UI Functions 224 | 225 | public static bool LoadBackups() { 226 | EnsureBackupFolderExists(); 227 | if (!File.Exists(Config.BackupCfg)) { //TODO: create blank config 228 | return false; 229 | } 230 | 231 | bool err = false; 232 | string json = File.ReadAllText(Config.BackupCfg); 233 | try { 234 | _baks = JsonConvert.DeserializeObject>(json); 235 | UpdateBackupList(); 236 | } catch (JsonSerializationException) { 237 | err = true; 238 | } catch (JsonReaderException) { 239 | err = true; 240 | } 241 | if (err) { 242 | DialogResult ans = Utility.ShowMsg( 243 | "The backup configuration file is corrupted. You may need to verify your game files on steam or reinstall." + 244 | "Would you like to delete the corrupted backup config file?", 245 | "Question" 246 | ); 247 | if (ans == DialogResult.Yes) { 248 | if (!Utility.DeleteFile(Config.BackupCfg)) { 249 | Utility.ShowMsg("The backup file could not be deleted. Is it open somewhere?", "Error"); 250 | } 251 | } 252 | } 253 | 254 | return true; 255 | } 256 | 257 | public static bool UpdateBackupList() { 258 | EnsureBackupFolderExists(); 259 | 260 | Program.MasterForm.bakListPanel.Controls.Clear(); 261 | foreach (KeyValuePair entry in _baks) { 262 | string entryName; 263 | if (Config.fullBakPath) { 264 | entryName = entry.Key; 265 | } else { 266 | entryName = entry.Value; 267 | } 268 | 269 | Panel container = new Panel { 270 | Width = Config.backupPanelWidth, 271 | Height = 17, 272 | Location = new Point(0, (Program.MasterForm.bakListPanel.Controls.Count * 20) + 1), 273 | }; 274 | container.MouseEnter += Program.MasterForm.ListPanel_rowHoverOn; 275 | container.MouseLeave += Program.MasterForm.ListPanel_rowHoverOff; 276 | 277 | CheckBox chb = new CheckBox { 278 | AutoSize = true, 279 | Text = Config.dirtyPadding + entryName, 280 | Location = Config.BackupsChbPoint 281 | }; 282 | chb.MouseEnter += Program.MasterForm.ListPanel_rowChildHoverOn; 283 | chb.MouseLeave += Program.MasterForm.ListPanel_rowChildHoverOff; 284 | 285 | container.Controls.Add(chb); 286 | Program.MasterForm.bakListPanel.Controls.Add(container); 287 | } 288 | return true; 289 | } 290 | 291 | #endregion 292 | 293 | #region Api Functions 294 | 295 | public static int CreateBackup(string path, bool overwrite) { 296 | EnsureBackupFolderExists(); 297 | string fileName = Path.GetFileNameWithoutExtension(path); 298 | string fileExt = Path.GetExtension(path); 299 | string bakPath = Config.Backup_dir + @"\" + fileName + fileExt; 300 | 301 | if (File.Exists(bakPath)) { 302 | if (!_baks.ContainsKey(path)) { // if file exists in backups folder but the original filepaths differ, rename 303 | int num = 0; 304 | do { 305 | num++; 306 | bakPath = Config.Backup_dir + @"\" + fileName + "(" + num + ")" + fileExt; 307 | } while (File.Exists(bakPath)); 308 | } 309 | } 310 | 311 | int res = Utility.CopyFile(path, bakPath, overwrite); 312 | if (res == 0 || res == 1) { 313 | _baks[path] = Path.GetFileName(bakPath); 314 | SaveBackups(); 315 | UpdateBackupList(); 316 | } 317 | return res; 318 | } 319 | 320 | public static bool RestoreBak(string filePath) { 321 | if (String.IsNullOrEmpty(filePath)) { 322 | return false; 323 | } 324 | 325 | int ret; 326 | try { 327 | ret = Utility.CopyFile(Config.Backup_dir + @"\" + _baks[filePath], filePath, true); 328 | } catch (KeyNotFoundException) { 329 | ret = 99; 330 | } 331 | if (ret == 0) { 332 | if (Config.DeleteOldBaks) { 333 | if (Utility.DeleteFile(Config.Backup_dir + @"\" + _baks[filePath])) { 334 | _baks.Remove(filePath); 335 | } else { 336 | Utility.ShowMsg("Could not remove old backup '" + _baks[filePath] + "'. Is the file open somewhere?", "Error"); 337 | } 338 | } 339 | return true; 340 | } else if (ret == 99) { 341 | Utility.ShowMsg("Could not locate a backup for the file at '" + filePath + "'.", "Error"); 342 | return false; 343 | } else { 344 | Utility.ShowMsg("Could not restore '" + _baks[filePath] + "'. If the game is open, close it and try again.", "Error"); 345 | return false; 346 | } 347 | } 348 | 349 | public static int RestoreBaks(List backupPathList) { 350 | EnsureBackupFolderExists(); 351 | 352 | if (backupPathList.Count == 0) { 353 | return 0; 354 | } 355 | 356 | Program.MasterForm.PBar_show(backupPathList.Count); 357 | bool chk = false; 358 | bool err = false; 359 | foreach (string path in backupPathList) { 360 | Program.MasterForm.PBar_update(); 361 | if (RestoreBak(path)) { 362 | chk = true; 363 | } else { 364 | err = true; 365 | } 366 | } 367 | SaveBackups(); 368 | UpdateBackupList(); 369 | Program.MasterForm.PBar_hide(); 370 | if (chk) { 371 | if (err) { 372 | return 1; // Partial success - Some files were restored 373 | } 374 | return 0; // Success - All files were restored 375 | } 376 | return 2; // Failure - No files were restored 377 | } 378 | 379 | public static bool DeleteAll(bool y) { 380 | EnsureBackupFolderExists(); 381 | 382 | if (!y) { 383 | DialogResult ans = Utility.ShowMsg("Are you sure you want to delete ALL of your backup(s)?\r\nNo crying afterwards?", "Question"); 384 | if (ans == DialogResult.No) { 385 | return true; 386 | } 387 | } 388 | 389 | bool err = false; 390 | Program.MasterForm.PBar_show(_baks.Count); 391 | List toDelete = new List(); 392 | foreach (KeyValuePair entry in _baks) { 393 | toDelete.Add(entry.Key); 394 | } 395 | if (toDelete.Count == 0) { 396 | Utility.ShowMsg("Nothing to delete.", "Info"); 397 | return true; 398 | } 399 | 400 | List remainingBaks = new List(); 401 | List requiredBaks = FilterNeededBackups(toDelete); 402 | foreach (string path in toDelete) { 403 | if (requiredBaks.Contains(path)) { 404 | remainingBaks.Add(path); 405 | continue; 406 | } 407 | if (!Utility.DeleteFile(Config.Backup_dir + @"\" + _baks[path])) { 408 | remainingBaks.Add(path); 409 | Utility.ShowMsg("Could not delete '" + path + "'. Is the file open somewhere?", "Error"); 410 | err = true; 411 | } 412 | } 413 | 414 | if (remainingBaks.Count == 0) { 415 | _baks = new Dictionary(); 416 | Utility.ShowMsg("All backups deleted.", "Info"); 417 | } else { 418 | Dictionary tmp = new Dictionary(); 419 | foreach (string path in remainingBaks) { // create backup config of files which couldn't be deleted 420 | tmp[path] = _baks[path]; 421 | } 422 | _baks = tmp; 423 | } 424 | SaveBackups(); 425 | UpdateBackupList(); 426 | Program.MasterForm.PBar_hide(); 427 | 428 | if (requiredBaks.Count != 0) { 429 | Utility.ShowMsg(requiredBaks.Count + " backups were not deleted because the original file(s) are currently patched with a mod. " + 430 | "Deleting these backups would make it impossible to unpatch the mod(s).", "Info"); 431 | } 432 | return !err; 433 | } 434 | public static bool DeleteBak(string path) { 435 | if (!_baks.ContainsKey(path)) { 436 | return false; 437 | } 438 | if (Utility.DeleteFile(Config.Backup_dir + @"\" + _baks[path])) { 439 | _baks.Remove(path); 440 | return true; 441 | } 442 | return false; 443 | } 444 | 445 | public static bool SaveBackups() { 446 | EnsureBackupFolderExists(); 447 | 448 | string json = JsonConvert.SerializeObject(_baks, Formatting.Indented); 449 | using (FileStream fs = File.Create(Config.BackupCfg)) { 450 | byte[] info = new UTF8Encoding(true).GetBytes(json); 451 | fs.Write(info, 0, info.Length); 452 | } 453 | return true; 454 | } 455 | 456 | #endregion 457 | 458 | #region Helper Functions 459 | 460 | private static bool EnsureBackupFolderExists() { 461 | if (!Directory.Exists(Config.Backup_dir)) { 462 | Directory.CreateDirectory(Config.Backup_dir); 463 | } 464 | 465 | return true; // C# is dumb. If we dont return something here it 'optimizes' and runs this asynchronously 466 | } 467 | 468 | public static string GetBakKey(string bakFileName) { 469 | foreach (KeyValuePair entry in _baks) { 470 | if (entry.Value == bakFileName) { 471 | return entry.Key; 472 | } 473 | } 474 | return null; 475 | } 476 | 477 | private static List FilterNeededBackups(List paths) { 478 | List requiredBaks = new List(); 479 | foreach (string enabledModpack in Config.GetEnabledModpacks()) { 480 | ModpackCfg modpackConfig = Modpacks.GetModpackConfig(enabledModpack); 481 | 482 | foreach (ModpackEntry entry in modpackConfig.entries) { 483 | foreach (string path in paths) { 484 | if (path == Utility.ExpandPath(entry.dest)) { 485 | requiredBaks.Add(path); 486 | } 487 | } 488 | } 489 | } 490 | 491 | return requiredBaks; 492 | } 493 | #endregion 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /Api/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Windows.Forms; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | using System.IO.Compression; 11 | using MCC_Mod_Manager.Api.Utilities; 12 | 13 | namespace MCC_Mod_Manager.Api { 14 | public class MainCfg { 15 | public string version = Config.version; 16 | public string MCC_version; 17 | public string MCC_home; 18 | public string backup_dir; 19 | public string modpack_dir; 20 | public bool deleteOldBaks; 21 | public Dictionary patched = new Dictionary(); 22 | } 23 | 24 | public class patchedEntry { 25 | public bool error; 26 | public Dictionary files = new Dictionary(); 27 | } 28 | 29 | static class Config { 30 | #region Config Fields 31 | public const string version = "v0.8"; 32 | private const string _cfgLocation = @".\MCC_Mod_Manager.cfg"; 33 | private const string _bakcfgName = @"\backups.cfg"; 34 | public const string _defaultReadmeText = @"Install using MCC Mod Manager: https://github.com/executionByFork/MCC_Mod_Manager/blob/master/README.md"; 35 | 36 | // UI elements 37 | public static string dirtyPadding = " "; 38 | public static readonly Point delBtnPoint = new Point(0, 3); 39 | public static readonly Point delBtnPointAlt = new Point(0, 15); 40 | public static readonly Point sourceTextBoxPoint = new Point(20, 1); 41 | public static readonly Point sourceBtnPoint = new Point(203, 0); 42 | public static readonly Point origTextBoxPoint = new Point(20, 26); 43 | public static readonly Point origBtnPoint = new Point(203, 25); 44 | public static readonly Point arrowPoint = new Point(245, -5); 45 | public static readonly Point arrowPointAlt = new Point(245, 7); 46 | public static readonly Point destTextBoxPoint = new Point(278, 1); 47 | public static readonly Point destTextBoxPointAlt = new Point(278, 14); 48 | public static readonly Point destBtnPoint = new Point(461, 0); 49 | public static readonly Point destBtnPointAlt = new Point(461, 13); 50 | public static readonly Font btnFont = new Font("Lucida Console", 10, FontStyle.Regular); 51 | public static readonly Font arrowFont = new Font("Reem Kufi", 12, FontStyle.Bold); 52 | 53 | public static readonly Point MyModsEnabledPoint = new Point(15, 1); 54 | public static readonly Point MyModsCautionPoint = new Point(37, 1); 55 | public static readonly Point MyModsChbPoint = new Point(66, 1); 56 | 57 | public static bool fullBakPath = false; 58 | public static int backupPanelWidth = 394; 59 | public static readonly Point BackupsChbPoint = new Point(30, 0); 60 | 61 | private static MainCfg _cfg = new MainCfg(); // this is set on form load 62 | 63 | #endregion 64 | 65 | #region Primary Config Mutators 66 | public static string MCC_version { 67 | get { 68 | return _cfg.MCC_version; 69 | } 70 | set { 71 | _cfg.MCC_version = value; 72 | } 73 | } 74 | public static string MCC_home { 75 | get { 76 | return _cfg.MCC_home; 77 | } 78 | set { 79 | _cfg.MCC_home = value; 80 | } 81 | } 82 | public static string Backup_dir { 83 | get { 84 | return _cfg.backup_dir; 85 | } 86 | set { 87 | _cfg.backup_dir = value; 88 | } 89 | } 90 | public static string BackupCfg { 91 | get { 92 | return _cfg.backup_dir + _bakcfgName; 93 | } 94 | } 95 | public static string Modpack_dir { 96 | get { 97 | return _cfg.modpack_dir; 98 | } 99 | set { 100 | _cfg.modpack_dir = value; 101 | } 102 | } 103 | public static bool DeleteOldBaks { 104 | get { 105 | return _cfg.deleteOldBaks; 106 | } 107 | set { 108 | _cfg.deleteOldBaks = value; 109 | } 110 | } 111 | 112 | public static Dictionary Patched { 113 | get { 114 | return _cfg.patched; 115 | } 116 | set { 117 | _cfg.patched = value; 118 | } 119 | } 120 | 121 | #endregion 122 | 123 | #region Event Handlers 124 | public static void BrowseFolderBtn_Click(object sender, EventArgs e) { 125 | var dialog = new FolderSelectDialog { 126 | InitialDirectory = Config.MCC_home, 127 | Title = "Select a folder" 128 | }; 129 | if (dialog.Show(Program.MasterForm.Handle)) { 130 | ((Button)sender).Parent.GetChildAtPoint(new Point(5, 3)).Text = dialog.FileName; 131 | } 132 | } 133 | 134 | public static void UpdateBtn_Click(object sender, EventArgs e) { 135 | if (string.IsNullOrEmpty(Program.MasterForm.cfgTextBox1.Text) || string.IsNullOrEmpty(Program.MasterForm.cfgTextBox2.Text) || string.IsNullOrEmpty(Program.MasterForm.cfgTextBox3.Text)) { 136 | Utility.ShowMsg("Config entries must not be empty.", "Error"); 137 | return; 138 | } 139 | 140 | if (!Config.ChkHomeDir(Program.MasterForm.cfgTextBox1.Text)) { 141 | Utility.ShowMsg("It seems you have selected the wrong MCC install directory. " + 142 | "Please make sure to select the folder named 'Halo The Master Chief Collection' in your Steam files.", "Error"); 143 | Program.MasterForm.cfgTextBox1.Text = Config.MCC_home; 144 | return; 145 | } 146 | Config.MCC_home = Program.MasterForm.cfgTextBox1.Text; 147 | Config.Backup_dir = Program.MasterForm.cfgTextBox2.Text; 148 | Config.Modpack_dir = Program.MasterForm.cfgTextBox3.Text; 149 | Config.DeleteOldBaks = Program.MasterForm.delOldBaks_chb.Checked; 150 | 151 | Config.SaveCfg(); 152 | 153 | Utility.ShowMsg("Config Updated!", "Info"); 154 | } 155 | 156 | public static void ResetApp_Click(object sender, EventArgs e) { 157 | DialogResult ans = Utility.ShowMsg("WARNING: This dangerous, and odds are you don't need to do it." + 158 | "\r\n\r\nThis button will reset the application state, so that the mod manager believes your Halo install is COMPLETELY unmodded. It will " + 159 | "delete ALL of your backups, and WILL NOT restore them beforehand. This is to reset the app to a default state and flush out any broken files." + 160 | "\r\n\r\nAre you sure you want to continue?", "Question"); 161 | if (ans == DialogResult.No) { 162 | return; 163 | } 164 | 165 | Config.DoResetApp(); 166 | } 167 | #endregion 168 | 169 | #region Api Functions 170 | 171 | public static void SaveCfg() { 172 | string json = JsonConvert.SerializeObject(_cfg, Formatting.Indented); 173 | using (FileStream fs = File.Create(_cfgLocation)) { 174 | byte[] info = new UTF8Encoding(true).GetBytes(json); 175 | fs.Write(info, 0, info.Length); 176 | } 177 | } 178 | 179 | public static int LoadCfg() { 180 | bool stabilize = false; 181 | bool needsStabilize = false; 182 | if (!File.Exists(_cfgLocation)) { 183 | CreateDefaultCfg(); 184 | } else { 185 | int r = ReadCfg(); 186 | if (r == 1) { 187 | DialogResult ans = Utility.ShowMsg("Your configuration has formatting errors, would you like to overwrite it with a default config?", "Question"); 188 | if (ans == DialogResult.No) { 189 | return 3; 190 | } 191 | CreateDefaultCfg(); 192 | } else if (r == 2) { 193 | DialogResult ans = Utility.ShowMsg("Your config file is using an old format, would you like to overwrite it with a default config?", "Question"); 194 | if (ans == DialogResult.No) { 195 | return 3; 196 | } 197 | CreateDefaultCfg(); 198 | } else { 199 | // check if game was updated 200 | if (MCC_version != GetCurrentBuild()) { 201 | DialogResult ans = Utility.ShowMsg("It appears that MCC has been updated. MCC Mod Manager needs to stabilize the game by uninstalling certain modpacks." + 202 | "\r\nWould you like to do this now? Selecting 'No' will disable features.", "Question"); 203 | if (ans == DialogResult.Yes) { 204 | stabilize = true; 205 | } else { 206 | needsStabilize = true; 207 | } 208 | } 209 | } 210 | } 211 | 212 | bool msg = false; 213 | List tmp = new List(); 214 | foreach (KeyValuePair modpack in Patched) { 215 | if (!Modpacks.VerifyExists(modpack.Key)) { 216 | if (!msg) { 217 | msg = true; 218 | Utility.ShowMsg("The '" + modpack.Key + "' modpack is missing from the modpacks folder. If this modpack is actually installed, " + 219 | "MCC Mod Manager won't be able to uninstall it. You should restore from backups or verify the game files through Steam." + 220 | "\r\nThis warning will only show once.", "Warning"); 221 | } 222 | tmp.Add(modpack.Key); 223 | } 224 | } 225 | foreach (string modpack in tmp) { 226 | RmPatched(modpack); 227 | } 228 | SaveCfg(); 229 | 230 | // Update config tab 231 | Program.MasterForm.cfgTextBox1.Text = MCC_home; 232 | Program.MasterForm.cfgTextBox2.Text = Backup_dir; 233 | Program.MasterForm.cfgTextBox3.Text = Modpack_dir; 234 | Program.MasterForm.delOldBaks_chb.Checked = DeleteOldBaks; 235 | 236 | if (stabilize) { 237 | return 1; 238 | } else if (needsStabilize) { 239 | return 2; 240 | } else { 241 | return 0; 242 | } 243 | } 244 | 245 | public static bool IsPatched(string modpackName) { 246 | try { 247 | return Patched.ContainsKey(modpackName); 248 | } catch (NullReferenceException) { 249 | return false; 250 | } 251 | } 252 | 253 | public static bool AddPatched(string modpackName) { 254 | Dictionary modfiles = new Dictionary(); 255 | ModpackCfg mCfg = Modpacks.GetModpackConfig(modpackName); 256 | using (ZipArchive archive = ZipFile.OpenRead(Config.Modpack_dir + @"\" + modpackName + ".zip")) { 257 | if (mCfg == null) { 258 | Utility.ShowMsg("Cannot set state to enabled. The file '" + modpackName + ".zip' is either not a compatible modpack or the config is corrupted.", "Error"); 259 | return false; 260 | } 261 | 262 | List patched = new List(); // track patched files in case of failure mid patch 263 | foreach (ModpackEntry entry in mCfg.entries) { 264 | modfiles[entry.dest] = Modpacks.GetMD5(Utility.ExpandPath(entry.dest)); 265 | } 266 | } 267 | 268 | Patched[modpackName] = new patchedEntry(); 269 | Patched[modpackName].files = modfiles; 270 | return true; 271 | } 272 | 273 | public static void RmPatched(string modpackName) { 274 | Patched.Remove(modpackName); 275 | } 276 | 277 | public static void DoResetApp() { 278 | Patched = new Dictionary(); 279 | MCC_version = GetCurrentBuild(); 280 | SaveCfg(); 281 | MyMods.LoadModpacks(); 282 | if (!Backups.DeleteAll(true)) { 283 | Utility.ShowMsg("There was an issue deleting at least one backup. Please delete these in the Backups tab to avoid restoring an old " + 284 | "version of the file in the future.", "Error"); 285 | } 286 | Backups.LoadBackups(); 287 | } 288 | 289 | #endregion 290 | 291 | #region Helper Functions 292 | 293 | public static List GetEnabledModpacks() { 294 | List list = new List(); 295 | 296 | foreach (KeyValuePair modpack in Patched) { 297 | list.Add(modpack.Key); 298 | } 299 | return list; 300 | } 301 | 302 | public static string GetCurrentBuild() { 303 | return Utility.ReadFirstLine(MCC_home + @"\build_tag.txt"); 304 | } 305 | 306 | private static int ReadCfg() { 307 | string json = File.ReadAllText(_cfgLocation); 308 | JObject jsonObject = null; 309 | try { 310 | jsonObject = JObject.Parse(json); 311 | } catch (JsonSerializationException) { 312 | return 1; 313 | } catch (JsonReaderException) { 314 | return 1; 315 | } 316 | //MainCfg values = JsonConvert.DeserializeObject(json); 317 | if (jsonObject.SelectToken("version") == null) { // error on pre v0.5 config 318 | return 2; 319 | } 320 | 321 | MCC_home = jsonObject.SelectToken("MCC_home").ToString(); 322 | if (jsonObject.SelectToken("MCC_version") == null) { // check for pre v0.7 config 323 | MCC_version = GetCurrentBuild(); 324 | } else { 325 | MCC_version = jsonObject.SelectToken("MCC_version").ToString(); 326 | } 327 | 328 | Backup_dir = jsonObject.SelectToken("backup_dir").ToString(); 329 | Modpack_dir = jsonObject.SelectToken("modpack_dir").ToString(); 330 | DeleteOldBaks = (bool)jsonObject.SelectToken("deleteOldBaks"); 331 | 332 | JObject tmp = (JObject)jsonObject.SelectToken("patched"); 333 | if (tmp == null || !tmp.HasValues) { // convert config patch object from v0.7 to v0.8 334 | Patched = new Dictionary(); 335 | } else { 336 | try { 337 | if (tmp.Properties().ElementAt(0).Value.SelectToken("error") == null) { 338 | Patched = new Dictionary(); 339 | var entries = JsonConvert.DeserializeObject>>(jsonObject.SelectToken("patched").ToString()); 340 | foreach (var entry in entries) { 341 | Patched[entry.Key] = new patchedEntry(); 342 | Patched[entry.Key].error = false; // assume no partial install modpack errors 343 | Patched[entry.Key].files = entry.Value; 344 | } 345 | } else { 346 | Patched = JsonConvert.DeserializeObject>(jsonObject.SelectToken("patched").ToString()); 347 | } 348 | } catch (JsonSerializationException) { 349 | return 1; 350 | } catch (JsonReaderException) { 351 | return 1; 352 | } 353 | } 354 | 355 | return 0; 356 | } 357 | 358 | public static bool CreateDefaultCfg() { 359 | _cfg = new MainCfg(); 360 | // default values declared here so that mainCfg class does not implicitly set defaults and bypass warning triggers 361 | MCC_home = @"C:\Program Files (x86)\Steam\steamapps\common\Halo The Master Chief Collection"; 362 | MCC_version = GetCurrentBuild(); // sets MCC_version to null if not found 363 | Backup_dir = @".\backups"; 364 | Modpack_dir = @".\modpacks"; 365 | DeleteOldBaks = false; 366 | Patched = new Dictionary(); 367 | 368 | SaveCfg(); 369 | Utility.ShowMsg("A default configuration file has been created. Please review and update it as needed.", "Info"); 370 | 371 | return true; 372 | } 373 | 374 | public static bool ChkHomeDir(String dir) { 375 | if (!File.Exists(dir + @"\MCC\Content\Paks\MCC-WindowsNoEditor.pak")) { 376 | return false; 377 | } 378 | if (!File.Exists(dir + @"\mcclauncher.exe")) { 379 | return false; 380 | } 381 | 382 | return true; 383 | } 384 | 385 | #endregion 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /Api/Modpacks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Windows.Forms; 7 | using System.Drawing; 8 | using System.Security.Cryptography; 9 | using Newtonsoft.Json; 10 | using MCC_Mod_Manager.Api.Utilities; 11 | 12 | namespace MCC_Mod_Manager.Api { 13 | public class ModpackEntry { 14 | public string src; 15 | public string orig; 16 | public string dest; 17 | public string type; 18 | } 19 | public class ModpackCfg { 20 | public string MCC_version; 21 | public List entries = new List(); 22 | } 23 | 24 | static class Modpacks { 25 | 26 | #region Event Handlers 27 | 28 | public static void AddRowButton_Click(object sender, EventArgs e) { 29 | OpenFileDialog ofd = new OpenFileDialog { 30 | InitialDirectory = "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}", // using the GUID to access 'This PC' folder 31 | Multiselect = true, 32 | Title = "Select one or more mod files:" 33 | }; 34 | 35 | if (ofd.ShowDialog() == DialogResult.OK) { 36 | foreach (string file in ofd.FileNames) { 37 | string type; 38 | if (Path.GetExtension(file) == ".asmp") { 39 | type = "alt"; 40 | } else { 41 | type = "normal"; 42 | } 43 | Program.MasterForm.createFilesPanel.Controls.Add(MakeRow(file, type)); 44 | } 45 | } 46 | } 47 | 48 | public static void readmeToggleButton_Click(object sender, EventArgs e) { 49 | if ((bool)Program.MasterForm.readmeToggleButton.Tag == false) { // Hide readme editing 50 | Program.MasterForm.readmeTxt.Enabled = false; 51 | Program.MasterForm.readmeTxt.Visible = false; 52 | Program.MasterForm.createFilesPanel.Visible = true; 53 | Program.MasterForm.addRowButton.Visible = true; 54 | Program.MasterForm.addRowButton.Enabled = true; 55 | Program.MasterForm.createLabel1.Text = "Modded File"; 56 | Program.MasterForm.createLabel2.Visible = true; 57 | 58 | Program.MasterForm.tt.SetToolTip(Program.MasterForm.readmeToggleButton, "Edit Readme"); 59 | Program.MasterForm.readmeToggleButton.Tag = true; // use tag to track UI state 60 | } else { // Reveal readme editing 61 | Program.MasterForm.readmeTxt.Enabled = true; 62 | Program.MasterForm.readmeTxt.Visible = true; 63 | Program.MasterForm.createFilesPanel.Visible = false; 64 | Program.MasterForm.addRowButton.Visible = false; 65 | Program.MasterForm.addRowButton.Enabled = false; 66 | Program.MasterForm.createLabel1.Text = "Editing README.txt"; 67 | Program.MasterForm.createLabel2.Visible = false; 68 | 69 | Program.MasterForm.tt.SetToolTip(Program.MasterForm.readmeToggleButton, "Manage files"); 70 | Program.MasterForm.readmeToggleButton.Tag = false; // use tag to track UI state 71 | } 72 | } 73 | 74 | public static void CreateModpackBtn_Click(object sender, EventArgs e) { 75 | string modpackName = Program.MasterForm.modpackName_txt.Text; 76 | IEnumerable modFilesList = Program.MasterForm.createFilesPanel.Controls.OfType(); 77 | if (modFilesList.Count() == 0) { 78 | Utility.ShowMsg("Please add at least one modded file entry", "Error"); 79 | return; 80 | } 81 | if (String.IsNullOrEmpty(modpackName)) { 82 | Utility.ShowMsg("Please enter a modpack name", "Error"); 83 | return; 84 | } 85 | 86 | List chk = new List(); 87 | ModpackCfg mCfg = new ModpackCfg { 88 | MCC_version = Utility.ReadFirstLine(Config.MCC_home + @"\build_tag.txt") 89 | }; 90 | foreach (Panel row in modFilesList) { 91 | string srcText = row.GetChildAtPoint(Config.sourceTextBoxPoint).Text; 92 | TextBox origTextbox = new TextBox(); // setting this to an empty value to avoid compile error on an impossible CS0165 93 | string destText; 94 | if ((string)row.Tag == "normal") { 95 | destText = row.GetChildAtPoint(Config.destTextBoxPoint).Text; 96 | } else { 97 | origTextbox = (TextBox)row.GetChildAtPoint(Config.origTextBoxPoint); 98 | destText = row.GetChildAtPoint(Config.destTextBoxPointAlt).Text; 99 | } 100 | 101 | if (string.IsNullOrEmpty(srcText) || string.IsNullOrEmpty(destText) || ((string)row.Tag == "alt" && string.IsNullOrEmpty(origTextbox.Text))) { 102 | Utility.ShowMsg("Filepaths cannot be empty.", "Error"); 103 | return; 104 | } 105 | if (!File.Exists(srcText)) { 106 | Utility.ShowMsg("The source file '" + srcText + "' does not exist.", "Error"); 107 | return; 108 | } 109 | if (!destText.StartsWith(Config.MCC_home)) { 110 | Utility.ShowMsg("Destination files must be located within the MCC install directory. " + 111 | "You may need to configure this directory if you haven't done so already.", "Error"); 112 | return; 113 | } 114 | if ((string)row.Tag == "alt" && origTextbox.Enabled && !origTextbox.Text.StartsWith(Config.MCC_home)) { 115 | Utility.ShowMsg("Unmodified map files must be selected at their default install location within the MCC install directory to allow the patch " + 116 | "to be correctly applied when this modpack is installed. The file you selected does not appear to lie inside the MCC install directory." + 117 | "\r\nYou may need to configure this directory if you haven't done so already.", "Error"); 118 | return; 119 | } 120 | string patchType; 121 | if (Path.GetExtension(srcText) == ".asmp") { 122 | patchType = "patch"; 123 | } else { 124 | bool isOriginalFile; 125 | try { 126 | isOriginalFile = Utility.IsHaloFile(Utility.CompressPath(destText)); 127 | } catch (JsonReaderException) { 128 | Utility.ShowMsg(@"MCC Mod Manager could not parse Formats\filetree.json", "Error"); 129 | return; 130 | } 131 | 132 | if (isOriginalFile) { 133 | patchType = "replace"; 134 | } else { 135 | patchType = "create"; 136 | } 137 | } 138 | 139 | mCfg.entries.Add(new ModpackEntry { 140 | src = srcText, 141 | orig = (patchType == "patch") ? Utility.CompressPath(origTextbox.Text) : null, 142 | dest = Utility.CompressPath(destText), // make modpack compatable with any MCC_home directory 143 | type = patchType 144 | }); 145 | chk.Add(destText); 146 | } 147 | 148 | if (chk.Distinct().Count() != chk.Count) { 149 | Utility.ShowMsg("You have multiple files trying to write to the same destination.", "Error"); 150 | return; 151 | } 152 | 153 | EnsureModpackFolderExists(); 154 | String modpackFilename = modpackName + ".zip"; 155 | String zipPath = Config.Modpack_dir + @"\" + modpackFilename; 156 | if (File.Exists(zipPath)) { 157 | Utility.ShowMsg("A modpack with that name already exists.", "Error"); 158 | return; 159 | } 160 | 161 | Program.MasterForm.PBar_show(mCfg.entries.Count); 162 | try { 163 | using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) { 164 | foreach (var entry in mCfg.entries) { 165 | Program.MasterForm.PBar_update(); 166 | String fileName = Path.GetFileName(entry.src); 167 | archive.CreateEntryFromFile(entry.src, fileName); // TODO: Fix issues when two source files have same name but diff path 168 | entry.src = fileName; // change src path to just modpack after archive creation but before json serialization 169 | } 170 | ZipArchiveEntry configFile = archive.CreateEntry("modpack_config.cfg"); 171 | string json = JsonConvert.SerializeObject(mCfg, Formatting.Indented); 172 | using (StreamWriter writer = new StreamWriter(configFile.Open())) { 173 | writer.WriteLine(json); 174 | } 175 | ZipArchiveEntry readmeFile = archive.CreateEntry("README.txt"); 176 | using (StreamWriter writer = new StreamWriter(readmeFile.Open())) { 177 | if (String.IsNullOrEmpty(Program.MasterForm.readmeTxt.Text)) { 178 | writer.WriteLine(Config._defaultReadmeText); 179 | } else { 180 | writer.WriteLine(Program.MasterForm.readmeTxt.Text); 181 | } 182 | } 183 | } 184 | } catch (NotSupportedException) { 185 | Utility.ShowMsg("The modpack name you have provided is not a valid filename on Windows.", "Error"); 186 | return; 187 | } 188 | 189 | Utility.ShowMsg("Modpack '" + modpackFilename + "' created.", "Info"); 190 | Program.MasterForm.PBar_hide(); 191 | ResetCreateModpacksTab(); 192 | MyMods.LoadModpacks(); 193 | return; 194 | } 195 | 196 | public static void DeleteRow(object sender, EventArgs e) { 197 | Program.MasterForm.createFilesPanel.Controls.Remove((Panel)((PictureBox)sender).Parent); 198 | RedrawCreatePanel(); 199 | } 200 | 201 | public static void ClearBtn_Click(object sender, EventArgs e) { 202 | ResetCreateModpacksTab(); 203 | } 204 | 205 | #endregion 206 | 207 | #region UI Functions 208 | 209 | private static void ResetCreateModpacksTab() { 210 | Program.MasterForm.createFilesPanel.Controls.Clear(); 211 | Program.MasterForm.modpackName_txt.Text = ""; 212 | Program.MasterForm.readmeTxt.Text = Config._defaultReadmeText; 213 | } 214 | 215 | public static Panel MakeRow(string filepath, string type) { 216 | PictureBox del = new PictureBox(); 217 | del.Image = del.ErrorImage; // bit of a hack to get the error image to appear 218 | del.Width = 14; 219 | del.Height = 16; 220 | del.MouseEnter += Program.MasterForm.BtnHoverOn; 221 | del.MouseLeave += Program.MasterForm.BtnHoverOff; 222 | del.Click += DeleteRow; 223 | if (type == "normal") { 224 | del.Location = Config.delBtnPoint; 225 | } else { 226 | del.Location = Config.delBtnPointAlt; 227 | } 228 | 229 | TextBox txt1 = new TextBox { 230 | Width = 180, 231 | Text = filepath, 232 | Location = Config.sourceTextBoxPoint 233 | }; 234 | 235 | Button btn1 = new Button { 236 | BackColor = SystemColors.ButtonFace, 237 | Width = 39, 238 | Font = Config.btnFont, 239 | Text = "...", 240 | Location = Config.sourceBtnPoint, 241 | Tag = "btn1" 242 | }; 243 | btn1.Click += Modpacks.Create_fileBrowse; 244 | Program.MasterForm.tt.SetToolTip(btn1, "Select modded file or .asmp file"); 245 | 246 | Label lbl = new Label { 247 | Width = 33, 248 | Font = Config.arrowFont, 249 | Text = ">>", 250 | Location = (type == "normal") ? Config.arrowPoint : Config.arrowPointAlt 251 | }; 252 | 253 | TextBox txt2 = new TextBox { 254 | Width = 180, 255 | Location = (type == "normal") ? Config.destTextBoxPoint : Config.destTextBoxPointAlt 256 | }; 257 | 258 | Button btn2 = new Button { 259 | BackColor = SystemColors.ButtonFace, 260 | Width = 39, 261 | Font = Config.btnFont, 262 | Text = "...", 263 | Location = (type == "normal") ? Config.destBtnPoint : Config.destBtnPointAlt, 264 | Tag = "btn2" 265 | }; 266 | btn2.Click += Modpacks.Create_fileBrowse; 267 | Program.MasterForm.tt.SetToolTip(btn2, "Select output location"); 268 | 269 | int offset = 5; 270 | foreach (Panel row in Program.MasterForm.createFilesPanel.Controls.OfType()) { 271 | if ((string)row.Tag == "normal") { 272 | offset += 27; 273 | } else { // Tagged as alt 274 | offset += 54; 275 | } 276 | } 277 | 278 | Panel newPanel = new Panel { 279 | Width = 500, 280 | Height = (type == "normal") ? 25 : 50, 281 | Location = new Point(10, offset), 282 | Tag = type 283 | }; 284 | 285 | if (type == "alt") { 286 | TextBox txt3 = new TextBox { 287 | Width = 180, 288 | Location = Config.origTextBoxPoint 289 | }; 290 | 291 | Button btn3 = new Button { 292 | BackColor = SystemColors.ButtonFace, 293 | Width = 39, 294 | Font = Config.btnFont, 295 | Text = "...", 296 | Location = Config.origBtnPoint, 297 | Tag = "btn3" 298 | }; 299 | btn3.Click += Modpacks.Create_fileBrowse; 300 | Program.MasterForm.tt.SetToolTip(btn3, "Select unmodified map file"); 301 | 302 | newPanel.Controls.Add(txt3); 303 | newPanel.Controls.Add(btn3); 304 | } 305 | 306 | newPanel.Controls.Add(del); 307 | newPanel.Controls.Add(txt1); 308 | newPanel.Controls.Add(btn1); 309 | newPanel.Controls.Add(lbl); 310 | newPanel.Controls.Add(txt2); 311 | newPanel.Controls.Add(btn2); 312 | 313 | return newPanel; 314 | } 315 | 316 | public static void SwapRowType(Panel p) { 317 | if ((string)p.Tag == "normal") { 318 | p.Height = 50; 319 | p.GetChildAtPoint(Config.delBtnPoint).Location = Config.delBtnPointAlt; 320 | // Retrieving label by coords doesn't work for some reason 321 | p.Controls.OfType