Advertisement
cirion5

Shadowrun Hong Kong Music Replacer: Swift for Mac

Dec 7th, 2017
154
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Swift 20.29 KB | None | 0 0
  1. //
  2. //  ViewController.swift
  3. //  SuperShadowrun
  4. //
  5. //  Copyright © 2017 Cirion. All rights reserved.
  6. //
  7.  
  8. import Cocoa
  9.  
  10. class ViewController: NSViewController, NSOpenSavePanelDelegate {
  11.    
  12.     let originalMusicFileSize = NSNumber.init(value: 107277012)
  13.     let newMusicFileSize = NSNumber.init(value: 133545950)
  14.    
  15.     let currentMusicFilePath = "/Contents/Data/resources.assets.resS";
  16.     let backupMusicFilePath = "/Contents/Data/resources.assets.resS.original";
  17.     let assetsFilePath = "/Contents/Data/resources.assets";
  18.    
  19.     // Offsets into resources.assets indicating where each track's size data is located.
  20.     // Note that these should each be on a 4-byte boundary. The difference between each offset can vary
  21.     // based on the length of the track name. The position data is always located 4 bytes after the size data.
  22.     let sizeOffsets: [UInt64] = [
  23.     2002734348, // Hub-TeaHouse
  24.     2002734404, // Hub-Exterior
  25.     2002734464, // Combat-Generic-Int2
  26.     2002734520, // Legwork-SLinterior
  27.     2002734572, // TitleTheme-UI  // INDEX 4: This is hard-coded to play over the main title screen.
  28.     2002734628, // Combat-Matrix2
  29.     2002734688, // Combat-Kowloon-Int2
  30.     2002734744, // Combat-Gobbet-Int1
  31.     2002734804, // Combat-Kowloon-WrapUp
  32.     2002734860, // Hub-SafeHouse
  33.     2002734920, // Combat-Is0bel-Int2
  34.     2002734972, // Legwork-Generic
  35.     2002735032, // Combat-Kowloon-Int1
  36.     2002735092, // Combat-Gobbet-WrapUp
  37.     2002735148, // Legwork-Is0bel
  38.     2002735204, // Legwork-Erhu
  39.     2002735260, // Legwork-Grendel
  40.     2002735324, // Legwork-ExitStageLeft
  41.     2002735368, // TESTSTINGER
  42.     2002735428, // Hub-Club88-ThroughWalls
  43.     2002735484, // Legwork-News
  44.     2002735536, // Combat-Boss
  45.     2002735584, // loudmusic
  46.     2002735644, // Legwork-Whistleblower
  47.     2002735704, // Combat-Is0bel-Int1
  48.     2002735764, // Combat-Generic-WrapUp
  49.     2002735824, // Combat-Generic-Int1
  50.     2002735880, // Hub-Club88-InStreet
  51.     2002735932, // Legwork-Kowloon
  52.     2002735992, // Combat-stinger-end
  53.     2002736060, // Combat-VictoriaHarbor-WrapUp
  54.     2002736120, // Combat-Grendel-Int1
  55.     2002736172, // Legwork-Museum
  56.     2002736220, // Sewer
  57.     2002736276, // Stealth-Matrix1
  58.     2002736332, // Legwork-Gobbet
  59.     2002736388, // Legwork-Hacking
  60.     2002736452, // Combat-Grendel-WrapUp
  61.     2002736512, // KnightKingsElevator
  62.     2002736572, // Legwork-VictoriaHarbor
  63.     2002736632, // Combat-Grendel-Int2
  64.     2002736692, // Combat-Is0bel-WrapUp
  65.     2002736760, // Combat-VictoriaHarbor-Int1
  66.     2002736820, // Combat-stinger-start
  67.     2002736880, // Combat-Gobbet-Int2
  68.     2002736936, // Club88-MainRoom
  69.     2002737000  // Combat-VictoriaHarbor-Int2
  70.     ]
  71.    
  72.     // The length in bytes of each original music track.
  73.     let originalSizeValues: [UInt32] = [
  74.         3124118, // Hub-TeaHouse
  75.         5637311, // Hub-Exterior
  76.         2455468, // Combat-Generic-Int2
  77.         2188997, // Legwork-SLinterior
  78.         3114666, // TitleTheme-UI
  79.         3634610, // Combat-Matrix2
  80.         2041430, // Combat-Kowloon-Int2
  81.         1584803, // Combat-Gobbet-Int1
  82.         2041354, // Combat-Kowloon-WrapUp
  83.         4940165, // Hub-SafeHouse
  84.         1561545, // Combat-Is0bel-Int2
  85.         3314926, // Legwork-Generic
  86.         2041479, // Combat-Kowloon-Int1
  87.         1587808, // Combat-Gobbet-WrapUp
  88.         2769366, // Legwork-Is0bel
  89.         2335314, // Legwork-Erhu
  90.         4081442, // Legwork-Grendel
  91.         2137171, // Legwork-ExitStageLeft
  92.         23526,   // TESTSTINGER
  93.         1945087, // Hub-Club88-ThroughWalls
  94.         814330,  // Legwork-News
  95.         2379429, // Combat-Boss
  96.         1461465, // loudmusic
  97.         2076460, // Legwork-Whistleblower
  98.         1561924, // Combat-Is0bel-Int1
  99.         2453965, // Combat-Generic-WrapUp
  100.         2456846, // Combat-Generic-Int1
  101.         1946422, // Hub-Club88-InStreet
  102.         4769051, // Legwork-Kowloon
  103.         174750,  // Combat-stinger-end
  104.         1555841, // Combat-VictoriaHarbor-WrapUp
  105.         2201208, // Combat-Grendel-Int1
  106.         3824152, // Legwork-Museum
  107.         1248239, // Sewer
  108.         3632784, // Stealth-Matrix1
  109.         3386400, // Legwork-Gobbet
  110.         1558730, // Legwork-Hacking
  111.         2204440, // Combat-Grendel-WrapUp
  112.         1722733, // KnightKingsElevator
  113.         2772684, // Legwork-VictoriaHarbor
  114.         2203725, // Combat-Grendel-Int2
  115.         1561357, // Combat-Is0bel-WrapUp
  116.         1556476, // Combat-VictoriaHarbor-Int1
  117.         98473,   // Combat-stinger-start
  118.         1587346, // Combat-Gobbet-Int2
  119.         1951436, // Club88-MainRoom
  120.         1555760  // Combat-VictoriaHarbor-Int2
  121.     ]
  122.    
  123.     // The index into the original resources.assets.resS at which each track can be located. Note that this is equivalent
  124.     // to a running total of the values in originalSizeValues because the tracks happen to be listed in order.
  125.     let originalPositionValues: [UInt32] = [
  126.         0,          // Hub-TeaHouse
  127.         3124118,    // Hub-Exterior
  128.         8761429,    // Combat-Generic-Int2
  129.         11216897,   // Legwork-SLinterior
  130.         13405894,   // TitleTheme-UI
  131.         16520560,   // Combat-Matrix2
  132.         20155170,   // Combat-Kowloon-Int2
  133.         22196600,   // Combat-Gobbet-Int1
  134.         23781403,   // Combat-Kowloon-WrapUp
  135.         25822757,   // Hub-SafeHouse
  136.         30762922,   // Combat-Is0bel-Int2
  137.         32324467,   // Legwork-Generic
  138.         35639393,   // Combat-Kowloon-Int1
  139.         37680872,   // Combat-Gobbet-WrapUp
  140.         39268680,   // Legwork-Is0bel
  141.         42038046,   // Legwork-Erhu
  142.         44373360,   // Legwork-Grendel
  143.         48454802,   // Legwork-ExitStageLeft
  144.         50591973,   // TESTSTINGER
  145.         50615499,   // Hub-Club88-ThroughWalls
  146.         52560586,   // Legwork-News
  147.         53374916,   // Combat-Boss
  148.         55754345,   // loudmusic
  149.         57215810,   // Legwork-Whistleblower
  150.         59292270,   // Combat-Is0bel-Int1
  151.         60854194,   // Combat-Generic-WrapUp
  152.         63308159,   // Combat-Generic-Int1
  153.         65765005,   // Hub-Club88-InStreet
  154.         67711427,   // Legwork-Kowloon
  155.         72480478,   // Combat-stinger-end
  156.         72655228,   // Combat-VictoriaHarbor-WrapUp
  157.         74211069,   // Combat-Grendel-Int1
  158.         76412277,   // Legwork-Museum
  159.         80236429,   // Sewer
  160.         81484668,   // Stealth-Matrix1
  161.         85117452,   // Legwork-Gobbet
  162.         88503852,   // Legwork-Hacking
  163.         90062582,   // Combat-Grendel-WrapUp
  164.         92267022,   // KnightKingsElevator
  165.         93989755,   // Legwork-VictoriaHarbor
  166.         96762439,   // Combat-Grendel-Int2
  167.         98966164,   // Combat-Is0bel-WrapUp
  168.         100527521,  // Combat-VictoriaHarbor-Int1
  169.         102083997,  // Combat-stinger-start
  170.         102182470,  // Combat-Gobbet-Int2
  171.         103769816,  // Club88-MainRoom
  172.         105721252   // Combat-VictoriaHarbor-Int2
  173.     ]
  174.    
  175.     // Current mapping.
  176.     /*
  177.      Hub-TeaHouse              -> ../music/vlc_converted/track0.ogg
  178.      Hub-Exterior              -> ../music/vlc_converted/track1.ogg
  179.      Combat-Generic-Int2       -> ../music/vlc_converted/track3.ogg
  180.      Legwork-SLinterior        -> ../music/vlc_converted/track5.ogg
  181.      TitleTheme-UI             -> invocationarray_acff/04siren.ogg
  182.      Combat-Matrix2            -> ../music/combat_matrix2.ogg
  183.      Combat-Kowloon-Int2       -> invocationarray_op/06catalyst.ogg
  184.      Combat-Gobbet-Int1        -> ../music/vlc_converted/track6.ogg
  185.      Combat-Kowloon-WrapUp     -> ../music/vlc_converted/track7.ogg
  186.      Hub-SafeHouse             -> ../music/vlc_converted/track9.ogg
  187.      Combat-Is0bel-Int2        -> ../music/vlc_converted/track12.ogg
  188.      Legwork-Generic           -> ../music/vlc_converted/track14.ogg
  189.      Combat-Kowloon-Int1       -> ../music/vlc_converted/track16.ogg
  190.      Combat-Gobbet-WrapUp      -> ../music/vlc_converted/track17.ogg
  191.      Legwork-Is0bel            -> ../music/vlc_converted/track18.ogg
  192.      Legwork-Erhu              -> ../music/vlc_converted/track22.ogg
  193.      Legwork-Grendel           -> ../music/vlc_converted/track24.ogg
  194.      Legwork-ExitStageLeft     -> ../music/vlc_converted/track26.ogg
  195.      TESTSTINGER               -> ../music/vlc_converted/track27.ogg
  196.      Hub-Club88-ThroughWalls   -> ../music/vlc_converted/track30.ogg
  197.      Legwork-News              -> ../music/vlc_converted/track32.ogg
  198.      Combat-Boss               -> ../music/vlc_converted/track33.ogg
  199.      loudmusic                 -> ../music/vlc_converted/track34.ogg
  200.      Legwork-Whistleblower     -> ../music/vlc_converted/track35.ogg
  201.      Combat-Is0bel-Int1        -> ../music/vlc_converted/track36.ogg
  202.      Combat-Generic-WrapUp     -> ../music/vlc_converted/track37.ogg
  203.      Combat-Generic-Int1       -> ../music/vlc_converted/track38.ogg
  204.      Hub-Club88-InStreet       -> ../music/vlc_converted/track39.ogg
  205.      Legwork-Kowloon           -> ../music/vlc_converted/track41.ogg
  206.      Combat-stinger-end        -> ../music/vlc_converted/track43.ogg
  207.      Combat-VictoriaHarbor-WrapUp -> ../music/vlc_converted/track49.ogg
  208.      Combat-Grendel-Int1       -> ../music/vlc_converted/track46.ogg
  209.      Legwork-Museum            -> ../music/vlc_converted/track47.ogg
  210.      Sewer                     -> ../music/vlc_converted/track48.ogg
  211.      Stealth-Matrix1           -> ../music/stealth_matrix1.ogg
  212.      Legwork-Gobbet            -> ../music/vlc_converted/track50.ogg
  213.      Legwork-Hacking           -> ../music/legwork_hacking.ogg
  214.      Combat-Grendel-WrapUp     -> ../music/vlc_converted/track53.ogg
  215.      KnightKingsElevator       -> ../music/vlc_converted/track51.ogg
  216.      Legwork-VictoriaHarbor    -> ../music/vlc_converted/track54.ogg
  217.      Combat-Grendel-Int2       -> ../music/vlc_converted/track55.ogg
  218.      Combat-Is0bel-WrapUp      -> ../music/vlc_converted/track57.ogg
  219.      Combat-VictoriaHarbor-Int1 -> ../music/vlc_converted/track58.ogg
  220.      Combat-stinger-start      -> ../music/vlc_converted/track59.ogg
  221.      Combat-Gobbet-Int2        -> ../music/vlc_converted/track60.ogg
  222.      Club88-MainRoom           -> invocationarray_acff/08withme.ogg
  223.      Combat-VictoriaHarbor-Int2 -> invocationarray_op/16catalyst.ogg
  224.      */
  225.    
  226.    
  227.     // The index into the new resources.assets.resS at which each track can be located. If using concat.py,
  228.     // these values can be taken from indices.txt. Pad out the array to 47 elements, you can safely reuse the
  229.     // same track multiple times. These do not necessarily need to be in ascending order.
  230.     let newPositionValues: [UInt32] = [
  231.         0,
  232.         1393879,
  233.         3014944,
  234.         4520349,
  235.         6075686,
  236.         13224667,
  237.         16859277,
  238.         23705366,
  239.         27333628,
  240.         30392989,
  241.         31785729,
  242.         36227626,
  243.         37558729,
  244.         39215240,
  245.         42753645,
  246.         48193852,
  247.         50059652,
  248.         52826294,
  249.         58756297,
  250.         60484053,
  251.         61866854,
  252.         64926321,
  253.         67814125,
  254.         69554962,
  255.         70868771,
  256.         74036566,
  257.         77274731,
  258.         78780769,
  259.         80649419,
  260.         82085067,
  261.         83502179,
  262.         85835133,
  263.         89345991,
  264.         90902926,
  265.         92457982,
  266.         96090766,
  267.         99115787,
  268.         100674517,
  269.         102037008,
  270.         104243280,
  271.         107256232,
  272.         111057445,
  273.         113768371,
  274.         115841178,
  275.         117621935,
  276.         118888656,
  277.         126481541
  278.     ]
  279.    
  280.     // The length in bytes of each new music track. Must match the order of tracks in newPositionValues. If using
  281.     // concat.py, these values can be taken from lengths.txt.
  282.     let newSizeValues: [UInt32] = [
  283.         1393879,
  284.         1621065,
  285.         1505405,
  286.         1555337,
  287.         7148981,
  288.         3634610,
  289.         6846089,
  290.         3628262,
  291.         3059361,
  292.         1392740,
  293.         4441897,
  294.         1331103,
  295.         1656511,
  296.         3538405,
  297.         5440207,
  298.         1865800,
  299.         2766642,
  300.         5930003,
  301.         1727756,
  302.         1382801,
  303.         3059467,
  304.         2887804,
  305.         1740837,
  306.         1313809,
  307.         3167795,
  308.         3238165,
  309.         1506038,
  310.         1868650,
  311.         1435648,
  312.         1417112,
  313.         2332954,
  314.         3510858,
  315.         1556935,
  316.         1555056,
  317.         3632784,
  318.         3025021,
  319.         1558730,
  320.         1362491,
  321.         2206272,
  322.         3012952,
  323.         3801213,
  324.         2710926,
  325.         2072807,
  326.         1780757,
  327.         1266721,
  328.         7592885,
  329.         7064409
  330.     ]
  331.  
  332.     //MARK: Properties
  333.    
  334.     @IBOutlet weak var selectButton: NSButtonCell!
  335.     @IBOutlet weak var filePathTextField: NSTextField!
  336.     @IBOutlet weak var convertButton: NSButtonCell!
  337.     @IBOutlet weak var restoreButton: NSButton!
  338.    
  339.     @IBAction func selectFile(_ sender: Any) {
  340.        
  341.         let dialog = NSOpenPanel();
  342.        
  343.         let initialPath = "~/Library/Application Support/Steam/steamapps/common/Shadowrun Hong Kong" as NSString
  344.         let expandedPath = initialPath.expandingTildeInPath;
  345.         dialog.directoryURL = NSURL.fileURL(withPath: expandedPath, isDirectory: true);
  346.        
  347.         dialog.title                   = "Select Shadowrun";
  348.         dialog.showsResizeIndicator    = true;
  349.         dialog.showsHiddenFiles        = true;
  350.         dialog.canChooseDirectories    = true;
  351.         dialog.canCreateDirectories    = false;
  352.         dialog.allowsMultipleSelection = false;
  353.         dialog.delegate = self;
  354.        
  355.         if (dialog.runModal() == NSModalResponseOK) {
  356.             let result = dialog.url // Pathname of the file
  357.            
  358.             if (result != nil) {
  359.                 let path = result?.path
  360.                 filePathTextField.stringValue = path!
  361.                 updateButtons()
  362.             }
  363.         } else {
  364.             // User clicked on "Cancel"
  365.             return
  366.         }
  367.     }
  368.    
  369.     func updateButtons() {
  370.         let fm = FileManager.default;
  371.         let appRoot = filePathTextField.stringValue
  372.         let currentMusicFile = appRoot + currentMusicFilePath
  373.         let backupMusicFile = appRoot + backupMusicFilePath
  374.         convertButton.isEnabled = fm.fileExists(atPath: currentMusicFile) && getFileSizeFor(path: currentMusicFile) != newMusicFileSize
  375.         restoreButton.isEnabled = fm.fileExists(atPath: backupMusicFile) && getFileSizeFor(path: currentMusicFile) != getFileSizeFor(path: backupMusicFile)
  376.     }
  377.    
  378.     func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
  379.         var isDir : ObjCBool = false
  380.         if (FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)) {
  381.             let fileName = (url.path as NSString).lastPathComponent;
  382.             return (isDir.boolValue && !fileName.contains(".app")) || (url.path as NSString).lastPathComponent == "SRHK.app"
  383.         }
  384.         return false
  385.     }
  386.    
  387.     override func viewDidLoad() {
  388.         super.viewDidLoad()
  389.        
  390.         convertButton.isEnabled = false
  391.         restoreButton.isEnabled = false
  392.         filePathTextField.isEditable = false
  393.     }
  394.  
  395.     override var representedObject: Any? {
  396.         didSet {
  397.         // Update the view, if already loaded.
  398.         }
  399.     }
  400.    
  401.     func showAlert(message: String) {
  402.         let alert = NSAlert();
  403.         alert.messageText = message;
  404.         alert.alertStyle = NSAlertStyle.informational;
  405.         alert.addButton(withTitle: "OK");
  406.         alert.runModal()
  407.     }
  408.    
  409.     func getFileSizeFor(path: String) -> NSNumber {
  410.         let fm = FileManager.default;
  411.         if (fm.fileExists(atPath: path)) {
  412.             let attributes = try! fm.attributesOfItem(atPath: path);
  413.             return (attributes[FileAttributeKey.size] as! NSNumber)
  414.         }
  415.         return 0
  416.     }
  417.    
  418.     @IBAction func restoreOriginalMusic(_ sender: Any) {
  419.         let appRoot = filePathTextField.stringValue
  420.         let currentMusicFile = appRoot + currentMusicFilePath
  421.         let backupMusicFile = appRoot + backupMusicFilePath
  422.         let assetsFile = appRoot + assetsFilePath
  423.         let fm = FileManager.default
  424.         // Shouldn't be possible since we only enable if the file exists, but just being extra-safe.
  425.         if (!fm.fileExists(atPath: backupMusicFile)) {
  426.             showAlert(message: "Backup does not exist. Aborting.")
  427.             return
  428.         }
  429.         if (getFileSizeFor(path: backupMusicFile) != originalMusicFileSize) {
  430.             showAlert(message: "Found a backup, but it was the wrong size. Please restore by verifying the integrity of Shadowrun Hong Kong within Steam.")
  431.             return
  432.         }
  433.        
  434.         do {
  435.             try fm.removeItem(atPath: currentMusicFile)
  436.         } catch {
  437.             showAlert(message: "Could not remove updated music. Aborting.")
  438.             return
  439.         }
  440.        
  441.         do {
  442.             try fm.copyItem(atPath: backupMusicFile, toPath: currentMusicFile)
  443.         } catch {
  444.             showAlert(message: "Could not restore backup. Please restore by verifying the integrity of Shadowrun Hong Kong within Steam.")
  445.             return
  446.         }
  447.         if (writeArrays(filePath: assetsFile, sizes: originalSizeValues, positions: originalPositionValues)) {
  448.             showAlert(message: "Restore successful! The original Hong Kong music will now play for all campaigns.")
  449.             updateButtons()
  450.         }
  451.     }
  452.    
  453.     func writeArrays(filePath: String, sizes: [UInt32], positions: [UInt32]) -> Bool {
  454.         // Edit the file.
  455.         if let fileHandle = FileHandle(forUpdatingAtPath: filePath) {
  456.            
  457.             for (index, sizeOffset) in sizeOffsets.enumerated() {
  458.                 var originalSizeValue = sizes[index]
  459.                 let originalSizeData = Data(bytes: &originalSizeValue, count: 4)
  460.                 fileHandle.seek(toFileOffset: sizeOffset)
  461.                 fileHandle.write(originalSizeData)
  462.                 var originalPositionValue = positions[index]
  463.                 let originalPositionData = Data(bytes: &originalPositionValue, count: 4)
  464.                 fileHandle.seek(toFileOffset: sizeOffset + 4)
  465.                 fileHandle.write(originalPositionData)
  466.             }
  467.            
  468.             // If writing multiple, only close after they're all done.
  469.             fileHandle.closeFile()
  470.             return true
  471.         } else {
  472.             showAlert(message: "Could not open resources.assets for editing. Aborting.")
  473.             return false
  474.         }
  475.        
  476.     }
  477.    
  478.     @IBAction func convert(_ sender: Any) {
  479.         let appRoot = filePathTextField.stringValue
  480.         let currentMusicFile = appRoot + currentMusicFilePath
  481.         let backupMusicFile = appRoot + backupMusicFilePath
  482.         let assetsFile = appRoot + assetsFilePath
  483.         let fm = FileManager.default
  484.         var canSkipBackup = false;
  485.         if (fm.fileExists(atPath: backupMusicFile)) {
  486.             let backupFileSize = getFileSizeFor(path: backupMusicFile)
  487.             if (backupFileSize == originalMusicFileSize) {
  488.                 canSkipBackup = true;
  489.             }
  490.         }
  491.         if (!canSkipBackup) {
  492.             let currentMusicFileSize = getFileSizeFor(path: currentMusicFile)
  493.             if (currentMusicFileSize != originalMusicFileSize) {
  494.                 showAlert(message: "WARNING: The current music file size appears incorrect. Please correct by verifying the integrity of Shadowrun Hong Kong within Steam. Aborting.")
  495.                 return;
  496.             }
  497.             do {
  498.                 try fm.copyItem(atPath: currentMusicFile, toPath: backupMusicFile);
  499.             } catch {
  500.                 showAlert(message: "Could not back up original music. Aborting.")
  501.                 return
  502.             }
  503.         }
  504.        
  505.         do {
  506.             try fm.removeItem(atPath: currentMusicFile)
  507.         } catch {
  508.             showAlert(message: "Could not remove original music. Aborting.")
  509.             return
  510.         }
  511.        
  512.         let bundle = Bundle.main
  513.         let resourcePath = bundle.path(forResource: "resources.assets", ofType: "resS")
  514.         do {
  515.             try fm.copyItem(atPath: resourcePath!, toPath: currentMusicFile)
  516.         } catch {
  517.             showAlert(message: "Could not copy new music. Aborting.")
  518.             return;
  519.         }
  520.        
  521.         if (writeArrays(filePath: assetsFile, sizes: newSizeValues, positions: newPositionValues)) {
  522.             showAlert(message: "Conversion successful! The new music will now play for all campaigns, including the official one.")
  523.             updateButtons()
  524.         }
  525.     }
  526.    
  527. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement