├── .gitignore ├── .gitmodules ├── LICENSE ├── Plug-Ins ├── Dialog Editor │ ├── Base.lproj │ │ └── DialogEditorWindow.xib │ ├── DITLDocumentView.swift │ ├── DITLItem.swift │ ├── DITLItemView.swift │ ├── DialogEditorWindowController.swift │ ├── Info.plist │ └── StdSystemIcons.rsrc ├── Hex Editor │ ├── Base.lproj │ │ └── HexWindow.xib │ ├── Hex Editor Cocoa-Bridging-Header.h │ ├── HexWindowController.swift │ └── Info.plist ├── Image Editor │ ├── Base.lproj │ │ └── ImageWindow.xib │ ├── ColorCursor.swift │ ├── ColorIcon.swift │ ├── ColorTable.swift │ ├── Icons.swift │ ├── ImageFormat.swift │ ├── ImageWindowController.swift │ ├── Info.plist │ ├── PackBits.swift │ ├── Picture.swift │ ├── PixelMap.swift │ ├── PixelPattern.swift │ ├── Pxm.swift │ ├── QDRect.swift │ ├── QTAnimation.swift │ ├── QTImageDesc.swift │ ├── QTNone.swift │ ├── QTPlanar.swift │ └── QTVideo.swift ├── Menu Editor │ ├── Base.lproj │ │ └── MenuEditorWindow.xib │ ├── Info.plist │ ├── Menu.swift │ ├── MenuEditorWindowController.swift │ ├── MenuItem.swift │ └── MenuItemTableRowView.swift ├── NovaTools │ ├── Extensions.swift │ ├── Galaxy Editor │ │ ├── GalaxyView.swift │ │ ├── GalaxyWindow.xib │ │ ├── GalaxyWindowController.swift │ │ ├── LinkingLayer.swift │ │ └── SystemView.swift │ ├── Info.plist │ ├── NovaTools.swift │ ├── PilotFilter.swift │ ├── Shan Editor │ │ ├── AltLayer.swift │ │ ├── Base.lproj │ │ │ └── ShanWindow.xib │ │ ├── BaseLayer.swift │ │ ├── EngineLayer.swift │ │ ├── ExitPoints.swift │ │ ├── LightLayer.swift │ │ ├── ShanView.swift │ │ ├── ShanWindowController.swift │ │ ├── ShieldLayer.swift │ │ ├── SpriteLayer.swift │ │ └── WeaponLayer.swift │ ├── SpobFilter.swift │ ├── Sprite Editor │ │ ├── Base.lproj │ │ │ └── SpriteWindow.xib │ │ ├── ShapeMachine.swift │ │ ├── Sprite.swift │ │ ├── SpriteImporter.swift │ │ ├── SpriteWindowController.swift │ │ └── SpriteWorld.swift │ ├── SteppingFieldDelegate.swift │ ├── System Editor │ │ ├── StellarView.swift │ │ ├── SystemMapView.swift │ │ ├── SystemWindow.xib │ │ └── SystemWindowController.swift │ └── Templates.rsrc └── Sound Editor │ ├── Info.plist │ ├── OSStatus.swift │ ├── Sound.swift │ ├── SoundResource.swift │ ├── SoundWindow.xib │ └── SoundWindowController.swift ├── README.md ├── RFSupport ├── AbstractEditor.swift ├── BinaryDataReader.swift ├── BinaryDataWriter.swift ├── CustomImageView.swift ├── Extensions.swift ├── Info.plist ├── MacRomanFormatter.swift ├── PluginRegistry.swift ├── ResForgePlugin.swift └── Resource.swift ├── ResForge.pxd ├── QuickLook │ ├── Icon.webp │ └── Thumbnail.webp ├── data │ ├── 2551F86C-2CD7-42EE-A263-E298E5965398 │ ├── 2BE19F4E-9E80-422A-B514-B56283E29634 │ ├── 61E81C9A-2AB9-4996-8680-1076F4588BA4 │ ├── 67C67F0B-2D50-4CBD-8ACB-B001F0079F23 │ ├── CEDC7F92-2B01-46DA-923C-0DFC5197ADF2 │ └── F0060113-5EA7-4EB2-85AB-8EF8AA454EF8 └── metadata.info ├── ResForge.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── ResForge (release).xcscheme │ └── ResForge.xcscheme └── ResForge ├── Assets.xcassets ├── Contents.json ├── Document Image.iconset │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ └── icon_32x32@2x.png ├── ResForge.appiconset │ ├── Contents.json │ ├── ResForge_128.png │ ├── ResForge_128@2x.png │ ├── ResForge_16.png │ ├── ResForge_16@2x.png │ ├── ResForge_256.png │ ├── ResForge_256@2x.png │ ├── ResForge_32.png │ ├── ResForge_32@2x.png │ ├── ResForge_512.png │ └── ResForge_512@2x.png ├── Resource Document.iconset │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ └── icon_512x512@2x.png ├── rectangle.and.pencil.and.ellipsis.imageset │ ├── Contents.json │ ├── rectangle.and.pencil.and.ellipsis.png │ └── rectangle.and.pencil.and.ellipsis@2x.png └── square.and.pencil.imageset │ ├── Contents.json │ ├── square.and.pencil.png │ └── square.and.pencil@2x.png ├── Classes ├── ApplicationDelegate.swift ├── BulkController.swift ├── CollectionController.swift ├── ConflictResolver.swift ├── CreateResourceController.swift ├── EditorManager.swift ├── ImportPanel.swift ├── InfoWindowController.swift ├── OpenPanelDelegate.swift ├── OutlineController.swift ├── PrefsController.swift ├── ResourceDataSource.swift ├── ResourceDirectory.swift ├── ResourceDocument.swift ├── ResourceExtension.swift ├── SelectTemplateController.swift ├── StandardController.swift ├── SupportRegistry.swift ├── TemplateDocument.swift └── TypeAttributesEditor.swift ├── Formats ├── AppleDoubleFormat.swift ├── AppleSingleFormat.swift ├── ClassicFormat.swift ├── ExtendedFormat.swift ├── MacBinaryFormat.swift ├── ResourceFileFormat.swift └── RezFormat.swift ├── Info.plist ├── Resources ├── Base.lproj │ ├── CreateResource.xib │ ├── InfoWindow.xib │ ├── MainMenu.xib │ ├── PrefsWindow.xib │ ├── ResourceDocument.xib │ ├── ResourceItem.xib │ └── SelectTemplate.xib └── en.lproj │ └── Credits.rtf └── Template Editor ├── Base.lproj └── TemplateWindow.xib ├── BaseElement.swift ├── CasedElement.swift ├── ElementList.swift ├── Elements ├── ElementAWRD.swift ├── ElementBB08.swift ├── ElementBBIT.swift ├── ElementBFLG.swift ├── ElementBHEX.swift ├── ElementBNDN.swift ├── ElementBOOL.swift ├── ElementBORV.swift ├── ElementBSKP.swift ├── ElementCASE.swift ├── ElementCASR.swift ├── ElementCHAR.swift ├── ElementCOLR.swift ├── ElementCSTR.swift ├── ElementDATE.swift ├── ElementDBYT.swift ├── ElementDOUB.swift ├── ElementDVDR.swift ├── ElementFBYT.swift ├── ElementFCNT.swift ├── ElementFIXD.swift ├── ElementHBYT.swift ├── ElementHEXD.swift ├── ElementKBYT.swift ├── ElementKCHR.swift ├── ElementKEYB.swift ├── ElementKHBT.swift ├── ElementKRID.swift ├── ElementKTYP.swift ├── ElementLSTB.swift ├── ElementOCNT.swift ├── ElementPACK.swift ├── ElementPNT.swift ├── ElementPSTR.swift ├── ElementREAL.swift ├── ElementRECT.swift ├── ElementRNAM.swift ├── ElementRREF.swift ├── ElementRSID.swift ├── ElementTNAM.swift ├── ElementUSTR.swift └── ElementWCOL.swift ├── KeyElement.swift ├── LinkingComboBox.swift ├── NCB ├── ElementNCB.swift ├── NCBExpression.swift ├── NCBSet.swift └── NCBTest.swift ├── README.md ├── RangedElement.swift ├── ResourceMappings.rsrc ├── TabbableOutlineView.swift ├── TemplateDataView.swift ├── TemplateEditor.swift ├── TemplateError.swift ├── TemplateLabelView.swift ├── TemplateParser.swift └── Templates.rsrc /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode user data 4 | *.xcworkspace/ 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "HexFiend"] 2 | path = HexFiend 3 | url = https://github.com/ridiculousfish/HexFiend.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrew Simmonds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plug-Ins/Dialog Editor/DITLDocumentView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | 4 | /// The "document area" of our scroll view, in which we show the DITL items. 5 | class DITLDocumentView: NSView { 6 | var dialogBounds: NSRect? 7 | var items: [DITLItemView] { 8 | subviews as? [DITLItemView] ?? [] 9 | } 10 | var controller: DialogEditorWindowController? { 11 | window?.windowController as? DialogEditorWindowController 12 | } 13 | @IBOutlet var widthConstraint: NSLayoutConstraint! 14 | @IBOutlet var heightConstraint: NSLayoutConstraint! 15 | 16 | override var isFlipped: Bool { true } 17 | override var acceptsFirstResponder: Bool { true } 18 | override var subviews: [NSView] { 19 | didSet { 20 | self.updateMinSize() 21 | } 22 | } 23 | 24 | override func draw(_ dirtyRect: NSRect) { 25 | if let dialogBounds { 26 | NSColor.white.setFill() 27 | dialogBounds.fill() 28 | NSColor.systemGray.setFill() 29 | dialogBounds.insetBy(dx: -1, dy: -1).frame() 30 | } 31 | } 32 | 33 | override func mouseDown(with event: NSEvent) { 34 | controller?.deselectAll(self) 35 | } 36 | 37 | func updateMinSize() { 38 | var minSize = dialogBounds?.size ?? NSSize() 39 | for item in items { 40 | let itemBox = item.frame 41 | minSize.width = max(itemBox.maxX, minSize.width) 42 | minSize.height = max(itemBox.maxY, minSize.height) 43 | } 44 | widthConstraint.constant = minSize.width + 16 45 | heightConstraint.constant = minSize.height + 16 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Plug-Ins/Dialog Editor/DITLItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | struct DITLItem { 5 | 6 | var itemView: DITLItemView 7 | var enabled: Bool 8 | var itemType: DITLItemType 9 | var resourceID: Int // Only SInt16, but let's be consistent with ResForge's Resource type. 10 | var helpItemType = DITLHelpItemType.unknown 11 | var itemNumber = Int16(0) 12 | } 13 | -------------------------------------------------------------------------------- /Plug-Ins/Dialog Editor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | 1 19 | NSPrincipalClass 20 | ${SWIFT_MODULE_NAME}.DialogEditorWindowController 21 | 22 | 23 | -------------------------------------------------------------------------------- /Plug-Ins/Dialog Editor/StdSystemIcons.rsrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/Plug-Ins/Dialog Editor/StdSystemIcons.rsrc -------------------------------------------------------------------------------- /Plug-Ins/Hex Editor/Hex Editor Cocoa-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | #import 7 | -------------------------------------------------------------------------------- /Plug-Ins/Hex Editor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | 1 19 | NSPrincipalClass 20 | ${SWIFT_MODULE_NAME}.HexWindowController 21 | 22 | 23 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/ColorCursor.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // https://developer.apple.com/library/archive/documentation/mac/pdf/ImagingWithQuickDraw.pdf#page=590 5 | 6 | struct ColorCursor { 7 | var imageRep: NSBitmapImageRep 8 | var format: ImageFormat 9 | } 10 | 11 | extension ColorCursor { 12 | init(_ reader: BinaryDataReader) throws { 13 | let crsr = try QDCCrsr(reader) 14 | try reader.setPosition(Int(crsr.crsrMap)) 15 | let pixMap = try PixelMap(reader) 16 | try reader.setPosition(Int(pixMap.pmTable)) 17 | let colorTable = try ColorTable.read(reader) 18 | try reader.setPosition(Int(crsr.crsrData)) 19 | let pixelData = try reader.readData(length: pixMap.pixelDataSize) 20 | imageRep = try pixMap.imageRep(pixelData: pixelData, colorTable: colorTable) 21 | try PixelMap.applyMask(crsr.crsrMask, to: imageRep, rowBytes: 2) 22 | format = pixMap.format 23 | } 24 | 25 | static func rep(_ data: Data, format: inout ImageFormat) -> NSBitmapImageRep? { 26 | let reader = BinaryDataReader(data) 27 | guard let crsr = try? Self(reader) else { 28 | return nil 29 | } 30 | format = crsr.format 31 | return crsr.imageRep 32 | } 33 | } 34 | 35 | struct QDCCrsr { 36 | static let size: UInt32 = 96 37 | static let typeMono: UInt16 = 0x8000 38 | static let typeColor: UInt16 = 0x8001 39 | var crsrType: UInt16 = Self.typeColor 40 | var crsrMap: UInt32 = Self.size 41 | var crsrData: UInt32 = Self.size + PixelMap.size 42 | var crsrXData: UInt32 = 0 43 | var crsrXValid: Int16 = 0 44 | var crsrXHandle: UInt32 = 0 45 | var crsr1Data: Data 46 | var crsrMask: Data 47 | var crsrHotSpot: QDPoint 48 | var crsrXTable: UInt32 = 0 49 | var crsrID: UInt32 = 0 50 | } 51 | 52 | extension QDCCrsr { 53 | init(_ reader: BinaryDataReader) throws { 54 | crsrType = try reader.read() 55 | crsrMap = try reader.read() 56 | crsrData = try reader.read() 57 | crsrXData = try reader.read() 58 | crsrXValid = try reader.read() 59 | crsrXHandle = try reader.read() 60 | crsr1Data = try reader.readData(length: 32) 61 | crsrMask = try reader.readData(length: 32) 62 | crsrHotSpot = try QDPoint(reader) 63 | crsrXTable = try reader.read() 64 | crsrID = try reader.read() 65 | guard crsrType == Self.typeColor, crsrMap != 0, crsrData != 0 else { 66 | throw ImageReaderError.invalid 67 | } 68 | } 69 | 70 | func write(_ writer: BinaryDataWriter) { 71 | writer.write(crsrType) 72 | writer.write(crsrMap) 73 | writer.write(crsrData) 74 | writer.write(crsrXData) 75 | writer.write(crsrXValid) 76 | writer.write(crsrXHandle) 77 | writer.writeData(crsr1Data) 78 | writer.writeData(crsrMask) 79 | crsrHotSpot.write(writer) 80 | writer.write(crsrXTable) 81 | writer.write(crsrID) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/ColorIcon.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // https://developer.apple.com/library/archive/documentation/mac/pdf/ImagingWithQuickDraw.pdf#page=378 5 | // https://developer.apple.com/library/archive/documentation/mac/pdf/More_Mac_Toolbox/Icon_Utilities.pdf#page=15 6 | 7 | struct ColorIcon { 8 | var imageRep: NSBitmapImageRep 9 | var format: ImageFormat = .unknown 10 | } 11 | 12 | extension ColorIcon { 13 | init(_ reader: BinaryDataReader) throws { 14 | let pixMap = try PixelMap(reader) 15 | let maskMap = try PixelMap(reader) 16 | let bitMap = try PixelMap(reader) 17 | try reader.advance(4) // Skip data handle 18 | let mask = try reader.readData(length: maskMap.pixelDataSize) 19 | try reader.advance(bitMap.pixelDataSize) // Skip bitmap data 20 | let colorTable = try ColorTable.read(reader) 21 | let pixelData = try reader.readData(length: pixMap.pixelDataSize) 22 | 23 | imageRep = try pixMap.imageRep(pixelData: pixelData, colorTable: colorTable) 24 | try maskMap.applyMask(mask, to: imageRep) 25 | format = pixMap.format 26 | } 27 | 28 | mutating func write(_ writer: BinaryDataWriter) throws { 29 | imageRep = ImageFormat.normalize(imageRep) 30 | let (pixMap, pixelData, palette) = try PixelMap.build(from: imageRep) 31 | let maskRowBytes = (imageRep.pixelsWide + 7) / 8 32 | let maskMap = PixelMap(rowBytes: maskRowBytes, bounds: pixMap.bounds) 33 | let mask = PixelMap.buildMask(from: imageRep) 34 | pixMap.write(writer) 35 | maskMap.write(writer) 36 | maskMap.write(writer) // Repeat maskMap for bitMap 37 | writer.advance(4) // Skip data handle 38 | writer.writeData(mask) 39 | writer.advance(maskRowBytes * imageRep.pixelsHigh) // Skip bitmap data 40 | ColorTable.write(writer, colors: palette) 41 | writer.writeData(pixelData) 42 | format = pixMap.format 43 | } 44 | 45 | static func rep(_ data: Data, format: inout ImageFormat) -> NSBitmapImageRep? { 46 | let reader = BinaryDataReader(data) 47 | guard let cicn = try? Self(reader) else { 48 | return nil 49 | } 50 | format = cicn.format 51 | return cicn.imageRep 52 | } 53 | 54 | static func data(from rep: NSBitmapImageRep, format: inout ImageFormat) throws -> Data { 55 | var cicn = Self(imageRep: rep) 56 | let writer = BinaryDataWriter() 57 | try cicn.write(writer) 58 | format = cicn.format 59 | return writer.data 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | 1 19 | NSPrincipalClass 20 | ${SWIFT_MODULE_NAME}.ImageWindowController 21 | 22 | 23 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/Pxm.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // `pxm#` resource used in old Mac OS X theme files 5 | // No documentation found 6 | 7 | struct Pxm { 8 | static let sharedMask: UInt16 = 0x1 9 | var imageRep: NSBitmapImageRep 10 | 11 | init(_ reader: BinaryDataReader) throws { 12 | let version = try reader.read() as UInt16 13 | let flags = try reader.read() as UInt16 14 | try reader.advance(4) 15 | let height = Int(try reader.read() as UInt16) 16 | let width = Int(try reader.read() as UInt16) 17 | try reader.advance(10) 18 | let count = Int(try reader.read() as UInt16) 19 | guard version == 3, 20 | width > 0, height > 0, count > 0 21 | else { 22 | throw ImageReaderError.invalid 23 | } 24 | 25 | // 1-bit click mask, round row bytes up to multiple of 2 26 | let maskRowBytes = (width + 15) / 16 * 2 27 | var maskSize = maskRowBytes * height 28 | if (flags & Self.sharedMask) == 0 { 29 | maskSize *= count 30 | } 31 | try reader.advance(maskSize) 32 | let bitmapSize = width * height * count * 4 33 | let rgbaData = try reader.readData(length: bitmapSize) 34 | 35 | imageRep = NSBitmapImageRep(bitmapDataPlanes: nil, 36 | pixelsWide: width, 37 | pixelsHigh: height * count, 38 | bitsPerSample: 8, 39 | samplesPerPixel: 4, 40 | hasAlpha: true, 41 | isPlanar: false, 42 | colorSpaceName: .deviceRGB, 43 | bytesPerRow: width * 4, 44 | bitsPerPixel: 32)! 45 | rgbaData.copyBytes(to: imageRep.bitmapData!, count: bitmapSize) 46 | } 47 | 48 | static func rep(_ data: Data) -> NSBitmapImageRep? { 49 | let reader = BinaryDataReader(data, bigEndian: false) 50 | return try? Self(reader).imageRep 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/QDRect.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | struct QDRect { 5 | var top: Int = 0 6 | var left: Int = 0 7 | var bottom: Int 8 | var right: Int 9 | } 10 | 11 | extension QDRect { 12 | init(_ reader: BinaryDataReader) throws { 13 | top = Int(try reader.read() as Int16) 14 | left = Int(try reader.read() as Int16) 15 | bottom = Int(try reader.read() as Int16) 16 | right = Int(try reader.read() as Int16) 17 | } 18 | 19 | init(for rep: NSBitmapImageRep) throws { 20 | guard rep.pixelsWide <= Int16.max, rep.pixelsHigh <= Int16.max else { 21 | throw ImageWriterError.tooBig 22 | } 23 | bottom = rep.pixelsHigh 24 | right = rep.pixelsWide 25 | } 26 | 27 | func write(_ writer: BinaryDataWriter) { 28 | writer.write(Int16(top)) 29 | writer.write(Int16(left)) 30 | writer.write(Int16(bottom)) 31 | writer.write(Int16(right)) 32 | } 33 | 34 | mutating func alignTo(_ point: QDPoint) { 35 | top -= point.y 36 | left -= point.x 37 | bottom -= point.y 38 | right -= point.x 39 | } 40 | 41 | func contains(_ other: QDRect) -> Bool { 42 | other.top >= top && 43 | other.left >= left && 44 | other.bottom <= bottom && 45 | other.right <= right 46 | } 47 | 48 | func contains(_ point: QDPoint) -> Bool { 49 | left.. top && right > left 64 | } 65 | 66 | func nsRect(in rep: NSBitmapImageRep) -> NSRect { 67 | return NSRect(x: left, y: rep.pixelsHigh - bottom, width: width, height: height) 68 | } 69 | } 70 | 71 | struct QDPoint { 72 | var v: Int 73 | var h: Int 74 | } 75 | 76 | extension QDPoint { 77 | init(_ reader: BinaryDataReader) throws { 78 | v = Int(try reader.read() as Int16) 79 | h = Int(try reader.read() as Int16) 80 | } 81 | 82 | func write(_ writer: BinaryDataWriter) { 83 | writer.write(Int16(v)) 84 | writer.write(Int16(h)) 85 | } 86 | 87 | var x: Int { 88 | get { h } 89 | set { h = newValue } 90 | } 91 | var y: Int { 92 | get { v } 93 | set { v = newValue } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/QTNone.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | /// Decoder for the QuickTime "raw " compressor. 5 | struct QTNone { 6 | static func rep(for imageDesc: QTImageDesc, reader: BinaryDataReader) throws -> NSBitmapImageRep { 7 | // Determine row bytes based on the data size and height 8 | let size = Int(imageDesc.dataSize) 9 | let rowBytes = size / Int(imageDesc.height) 10 | 11 | // Read the data and create the image rep 12 | try reader.advance(imageDesc.bytesUntilData) 13 | let data = try reader.readData(length: size) 14 | return try imageDesc.blitter(rowBytes: rowBytes).imageRep(pixelData: data, colorTable: imageDesc.colorTable) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Plug-Ins/Image Editor/QTPlanar.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // https://wiki.multimedia.cx/index.php/8BPS 5 | 6 | /// Decoder for the QuickTime "8BPS" (Adobe Photoshop) compressor. 7 | struct QTPlanar { 8 | static func rep(for imageDesc: QTImageDesc, reader: BinaryDataReader) throws -> NSBitmapImageRep { 9 | let width = Int(imageDesc.width) 10 | let height = Int(imageDesc.height) 11 | let depth = imageDesc.resolvedDepth 12 | 13 | // Start by inferring the channel count from the depth 14 | var channelCount = max(depth / 8, 1) 15 | // Parse the remaining atoms to see if there's an explicit channel count 16 | var remaining = imageDesc.bytesUntilData 17 | while remaining >= 10 { 18 | let size = Int(try reader.read() as UInt32) 19 | let type = try reader.read() as UInt32 20 | if type.fourCharString == "chct" { 21 | channelCount = Int(try reader.read() as UInt16) 22 | remaining -= 10 23 | break 24 | } 25 | try reader.advance(size - 8) 26 | remaining -= size 27 | } 28 | try reader.advance(remaining) 29 | 30 | var data: Data 31 | if imageDesc.version == 0 { 32 | // Uncompressed 33 | data = try reader.readData(length: Int(imageDesc.dataSize)) 34 | } else { 35 | // PackBits - all line counts are stored first 36 | var packLength = 0 37 | for _ in 0..<(height * channelCount) { 38 | packLength += Int(try reader.read() as UInt16) 39 | } 40 | let packed = try reader.readData(length: packLength) 41 | let outputSize = width * height * channelCount 42 | data = Data(repeating: 0, count: outputSize) 43 | try data.withUnsafeMutableBytes { output in 44 | try packed.withUnsafeBytes { input in 45 | try PackBits.decode(input, to: output.baseAddress!, outputSize: outputSize) 46 | } 47 | } 48 | } 49 | 50 | switch depth { 51 | case 1, 8: 52 | // Determine row bytes based on width and depth 53 | let rowBytes = (width * depth + 7) / 8 54 | // Monochrome images don't specify a clut - fallback to system1 55 | let colorTable = imageDesc.colorTable ?? ColorTable.system1 56 | return try imageDesc.blitter(rowBytes: rowBytes).imageRep(pixelData: data, colorTable: colorTable) 57 | case 24, 32: 58 | // Construct a 3-channel planar image rep, ignoring any additional channels in the data 59 | let size = width * height * 3 60 | guard data.count >= size else { 61 | throw ImageReaderError.invalid 62 | } 63 | let rep = NSBitmapImageRep(bitmapDataPlanes: nil, 64 | pixelsWide: width, 65 | pixelsHigh: height, 66 | bitsPerSample: 8, 67 | samplesPerPixel: 3, 68 | hasAlpha: false, 69 | isPlanar: true, 70 | colorSpaceName: .deviceRGB, 71 | bytesPerRow: width, 72 | bitsPerPixel: 0)! 73 | data.copyBytes(to: rep.bitmapData!, count: size) 74 | return rep 75 | default: 76 | // Depths 2, 4 and 16 are not known to be valid 77 | throw ImageReaderError.invalid 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Plug-Ins/Menu Editor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | 1 19 | NSPrincipalClass 20 | ${SWIFT_MODULE_NAME}.MenuEditorWindowController 21 | 22 | 23 | -------------------------------------------------------------------------------- /Plug-Ins/Menu Editor/MenuItemTableRowView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class MenuItemTableRowView : NSTableRowView { 4 | 5 | enum RowStyle { 6 | case titleCell 7 | case firstItemCell 8 | case lastItemCell 9 | case middleCell // most cells 10 | case onlyCell 11 | } 12 | 13 | enum ContentStyle { 14 | case normal 15 | case separator 16 | case submenu // Like normal, but with an arrow on the right. 17 | } 18 | 19 | var rowStyle = RowStyle.middleCell { 20 | didSet { 21 | self.needsDisplay = true 22 | } 23 | } 24 | var contentStyle = ContentStyle.normal { 25 | didSet { 26 | self.needsDisplay = true 27 | } 28 | } 29 | 30 | override func draw(_ dirtyRect: NSRect) { 31 | var box = bounds.insetBy(dx: 2, dy: 0) 32 | 33 | NSColor.textColor.setStroke() 34 | 35 | switch rowStyle { 36 | 37 | case .titleCell: 38 | box.origin.x += 16 // keep these two lines in sync with XIB "Mark" and "Shortcut" column widths. 39 | box.size.width -= 16 + 25; 40 | if !isSelected { 41 | NSColor.textColor.setFill() 42 | } else { 43 | NSColor.selectedMenuItemColor.setFill() 44 | } 45 | NSBezierPath.fill(box) 46 | case .lastItemCell: 47 | box.origin.y -= 10 + 1 // 1 extra for shadow. 48 | box.size.height += 10 49 | var shadowBox = box.offsetBy(dx: 2, dy: 1) 50 | shadowBox.size.width -= 1 51 | NSColor.textColor.setFill() 52 | NSBezierPath.fill(shadowBox) 53 | if !isSelected { 54 | NSColor.textBackgroundColor.setFill() 55 | } else { 56 | NSColor.selectedMenuItemColor.setFill() 57 | } 58 | NSBezierPath.fill(box) 59 | NSBezierPath.stroke(box.insetBy(dx: 0.5, dy: 0.5)) 60 | case .firstItemCell: 61 | box.size.height += 20 62 | let shadowBox = box.offsetBy(dx: 1, dy: 2) 63 | NSColor.textColor.setFill() 64 | NSBezierPath.fill(shadowBox) 65 | if !isSelected { 66 | NSColor.textBackgroundColor.setFill() 67 | } else { 68 | NSColor.selectedMenuItemColor.setFill() 69 | } 70 | NSBezierPath.fill(box) 71 | NSBezierPath.stroke(box.insetBy(dx: 0.5, dy: 0.5)) 72 | case .middleCell: 73 | box.origin.y -= 10 74 | box.size.height += 20 75 | let shadowBox = box.offsetBy(dx: 1, dy: 1) 76 | NSColor.textColor.setFill() 77 | NSBezierPath.fill(shadowBox) 78 | if !isSelected { 79 | NSColor.textBackgroundColor.setFill() 80 | } else { 81 | NSColor.selectedMenuItemColor.setFill() 82 | } 83 | NSBezierPath.fill(box) 84 | NSBezierPath.stroke(box.insetBy(dx: 0.5, dy: 0.5)) 85 | case .onlyCell: 86 | box.size.height -= 1 87 | var shadowBox = box.offsetBy(dx: 1, dy: 1) 88 | shadowBox.origin.x += 1 89 | shadowBox.size.width -= 1 90 | shadowBox.origin.y += 1 91 | shadowBox.size.height -= 1 92 | NSColor.textColor.setFill() 93 | NSBezierPath.fill(shadowBox) 94 | if !isSelected { 95 | NSColor.textBackgroundColor.setFill() 96 | } else { 97 | NSColor.selectedMenuItemColor.setFill() 98 | } 99 | NSBezierPath.fill(box) 100 | NSBezierPath.stroke(box.insetBy(dx: 0.5, dy: 0.5)) 101 | } 102 | 103 | if contentStyle == .separator { 104 | let contentBox = bounds.insetBy(dx: 2, dy: 0) 105 | NSColor.lightGray.setStroke() 106 | NSBezierPath.strokeLine(from: NSPoint(x: contentBox.minX + 1, y: trunc(contentBox.midY) + 0.5), to: NSPoint(x: contentBox.maxX - 1, y: trunc(contentBox.midY) + 0.5)) 107 | } else if contentStyle == .submenu { 108 | var contentBox = bounds.insetBy(dx: 3, dy: 3) 109 | contentBox.size.width = contentBox.size.height / 2.0 110 | contentBox.origin.x = bounds.maxX - 6 - contentBox.size.width 111 | let triangle = NSBezierPath() 112 | triangle.move(to: contentBox.origin) 113 | triangle.line(to: NSPoint(x: contentBox.origin.x, y: contentBox.maxY)) 114 | triangle.line(to: NSPoint(x: contentBox.maxX, y: contentBox.midY)) 115 | triangle.line(to: contentBox.origin) 116 | NSColor.textColor.setFill() 117 | triangle.fill() 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Extensions.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension AffineTransform { 4 | /// Applies the affine transform to the specified rectangle. 5 | func transform(_ rect: NSRect) -> NSRect { 6 | NSRect(origin: transform(rect.origin), size: transform(rect.size)) 7 | } 8 | } 9 | 10 | extension NSPoint { 11 | // NSPoint only directly conforms to Hashable since macOS 15 12 | struct Hash: Hashable { 13 | let x: Double 14 | let y: Double 15 | } 16 | var hashable: Hash { .init(x: x, y: y) } 17 | 18 | /// Returns the nearest point to this one that lies within the given rectangle. 19 | func constrained(within rect: NSRect) -> Self { 20 | Self(x: min(max(x, rect.minX), rect.maxX), y: min(max(y, rect.minY), rect.maxY)) 21 | } 22 | } 23 | 24 | extension NSRect { 25 | /// The center point of the specified rectangle. 26 | var center: NSPoint { 27 | get { .init(x: midX, y: midY) } 28 | set { 29 | origin.x = newValue.x - width / 2 30 | origin.y = newValue.y - height / 2 31 | } 32 | } 33 | } 34 | 35 | extension NSView { 36 | /// Scrolls the view’s closest ancestor NSClipView object so a point in the view lies at the center of clip view's bounds rectangle. 37 | func centerScroll(_ point: NSPoint) { 38 | if let rect = enclosingScrollView?.documentVisibleRect { 39 | self.scroll(NSPoint(x: point.x - rect.width / 2, y: point.y - rect.height / 2)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Galaxy Editor/LinkingLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class LinkingLayer: CAShapeLayer { 4 | var source: SystemView? 5 | var targets: [SystemView] = [] 6 | var target: SystemView? { 7 | // Prefer the rightmost system when there are multiple under the cursor 8 | targets.max { $0.point.x < $1.point.x } 9 | } 10 | private var flagsMonitor: Any? 11 | 12 | override init() { 13 | super.init() 14 | // Initial state for transition 15 | fillColor = nil 16 | strokeColor = nil 17 | lineWidth = 4 18 | // Watch event flags to see if the colour needs to change 19 | flagsMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in 20 | self?.setNeedsDisplay() 21 | return event 22 | } 23 | } 24 | 25 | override init(layer: Any) { 26 | super.init(layer: layer) 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | deinit { 34 | if let flagsMonitor { 35 | NSEvent.removeMonitor(flagsMonitor) 36 | } 37 | } 38 | 39 | override func display() { 40 | guard let source, let window = source.window else { 41 | fillColor = nil 42 | strokeColor = nil 43 | lineWidth = 4 44 | // Allow fade out if target set, otherwise clear immediately 45 | if targets.isEmpty { 46 | path = nil 47 | } 48 | return 49 | } 50 | 51 | let remove = NSEvent.modifierFlags.contains(.option) 52 | let color: NSColor = if target == nil { 53 | remove ? .systemOrange : .systemBlue 54 | } else { 55 | remove ? .systemRed : .systemGreen 56 | } 57 | fillColor = color.cgColor 58 | strokeColor = color.cgColor 59 | lineWidth = 2 60 | 61 | let path = CGMutablePath() 62 | let sourceRect = CGRect(origin: source.point, size: .zero).insetBy(dx: -3, dy: -3) 63 | path.addEllipse(in: sourceRect) 64 | path.move(to: source.point) 65 | if let target { 66 | path.addLine(to: target.point) 67 | let targetRect = CGRect(origin: target.point, size: .zero).insetBy(dx: -3, dy: -3) 68 | path.addEllipse(in: targetRect) 69 | } else { 70 | let to = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) 71 | path.addLine(to: to) 72 | } 73 | self.path = path 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | 1 19 | NSPrincipalClass 20 | ${SWIFT_MODULE_NAME}.NovaTools 21 | 22 | 23 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/NovaTools.swift: -------------------------------------------------------------------------------- 1 | import RFSupport 2 | 3 | class NovaTools: PlaceholderProvider { 4 | static var supportedTypes = ["dësc"] 5 | 6 | static func placeholderName(for resource: Resource) -> String? { 7 | switch resource.typeCode { 8 | case "dësc": 9 | guard let end = resource.data.firstIndex(of: 0) else { 10 | return nil 11 | } 12 | let data = resource.data.prefix(upTo: end).prefix(100) 13 | return String(data: data, encoding: .macOSRoman) 14 | default: 15 | return nil 16 | } 17 | } 18 | } 19 | 20 | extension NovaTools: TypeIconProvider { 21 | static var typeIcons = [ 22 | "bööm": "💥", 23 | "chär": "🧑‍🚀", 24 | "cölr": "🎨", 25 | "crön": "⏱️", 26 | "dësc": "💬", 27 | "düde": "👱‍♂️", 28 | "flët": "🚢", 29 | "gövt": "🏴‍☠️", 30 | "ïntf": "🔘", 31 | "jünk": "💎", 32 | "mïsn": "📦", 33 | "nëbu": "🦠", 34 | "öops": "💩", 35 | "oütf": "🔧", 36 | "përs": "👤", 37 | "ränk": "🎖️", 38 | "rlëD": "🎬", 39 | "röid": "☄️", 40 | "shän": "🪄", 41 | "shïp": "🚀", 42 | "spïn": "🌀", 43 | "spöb": "🪐", 44 | "sÿst": "💫", 45 | "wëap": "🔫", 46 | "l33t": "🤡", 47 | ] 48 | } 49 | 50 | extension ResourceType { 51 | static let nebula = Self("nëbu") 52 | static let rle16 = Self("rlëD") 53 | static let spin = Self("spïn") 54 | static let spaceObject = Self("spöb") 55 | static let system = Self("sÿst") 56 | } 57 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/PilotFilter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class PilotFilter: TemplateFilter { 5 | static let supportedTypes = ["NpïL"] 6 | static let name = "Pilot Decrypter" 7 | 8 | static func filter(data: Data, for resourceType: String) -> Data { 9 | var magic: UInt32 = 0xB36A210F 10 | // Work through 4 bytes at a time by converting to [UInt32] and back 11 | var newData = data.withUnsafeBytes({ Array($0.bindMemory(to: UInt32.self)) }).map({ i -> UInt32 in 12 | let j = i ^ magic.bigEndian 13 | magic &+= 0xDEADBEEF 14 | magic ^= 0xDEADBEEF 15 | return j 16 | }).withUnsafeBufferPointer({ Data(buffer: $0) }) 17 | // Work through remaining bytes 18 | for i in data.dropFirst(newData.count) { 19 | newData.append(i ^ UInt8(magic >> 24)) 20 | magic <<= 8 21 | } 22 | return newData 23 | } 24 | 25 | static func unfilter(data: Data, for resourceType: String) -> Data { 26 | // Encryption and decryption are the same 27 | return Self.filter(data: data, for: resourceType) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/AltLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class AltLayer: BaseLayer { 4 | override var currentFrame: NSBitmapImageRep? { 5 | guard frames.count > 0 else { 6 | return nil 7 | } 8 | let index = (controller.framesPerSet * currentSet) + controller.currentFrame 9 | return frames[index % frames.count] 10 | } 11 | @objc dynamic var setCount = 0 12 | @objc dynamic var hideDisabled = false 13 | private var currentSet = 0 14 | private var setTicks = 0 15 | 16 | override func nextFrame() { 17 | if enabled && setCount > 0 { 18 | setTicks += 1 19 | if setTicks >= controller.animationDelay { 20 | currentSet = (currentSet + 1) % setCount 21 | setTicks = 0 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/BaseLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class BaseLayer: SpriteLayer { 4 | override var operation: NSCompositingOperation { .sourceOver } 5 | override var alpha: CGFloat { 6 | get { 7 | if !enabled { 8 | return 0 9 | } 10 | return CGFloat(32-controller.baseTransparency) * Self.TransparencyStep 11 | } 12 | set { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/EngineLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class EngineLayer: ShieldLayer { 4 | override var alpha: CGFloat { 5 | get { 6 | // A rough approximation of Nova's engine flicker 7 | return super.alpha + CGFloat.random(in: -0.2..<0) 8 | } 9 | set { } 10 | } 11 | 12 | override func nextFrame() { 13 | if enabled && super.alpha < 1 { 14 | super.alpha += Self.TransparencyStep 15 | } else if !enabled && super.alpha > 0 { 16 | super.alpha -= Self.TransparencyStep 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/ExitPoints.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ExitPoints: NSObject { 4 | @objc var enabled = false 5 | @objc var point1 = ExitPoint() 6 | @objc var point2 = ExitPoint() 7 | @objc var point3 = ExitPoint() 8 | @objc var point4 = ExitPoint() 9 | let points: [ExitPoint] 10 | private let color: NSColor 11 | 12 | init(_ color: NSColor) { 13 | points = [ 14 | point1, 15 | point2, 16 | point3, 17 | point4 18 | ] 19 | self.color = color 20 | } 21 | 22 | func draw(_ transform: AffineTransform) { 23 | guard enabled else { 24 | return 25 | } 26 | color.setFill() 27 | let size = NSSize(width: 3, height: 3) 28 | for point in points { 29 | var origin = NSPoint(x: point.x, y: point.y) 30 | origin = transform.transform(origin) 31 | origin.x.round() 32 | origin.y.round() 33 | origin.x -= 1 34 | origin.y += point.z - 1 35 | NSRect(origin: origin, size: size).frame() 36 | } 37 | } 38 | } 39 | 40 | class ExitPoint: NSObject { 41 | weak var controller: ShanWindowController? 42 | @objc dynamic var x: CGFloat = 0 43 | @objc dynamic var y: CGFloat = 0 44 | @objc dynamic var z: CGFloat = 0 45 | 46 | override func didChangeValue(forKey key: String) { 47 | super.didChangeValue(forKey: key) 48 | controller?.setDocumentEdited(true) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/LightLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class LightLayer: SpriteLayer { 4 | // These labels are also bound to the hide vars so 'weak' is needed to prevent ref cycles 5 | @IBOutlet weak var labelA: NSTextField! 6 | @IBOutlet weak var labelB: NSTextField! 7 | @IBOutlet weak var labelC: NSTextField! 8 | @IBOutlet weak var labelD: NSTextField! 9 | @objc dynamic var blinkMode: Int16 = 0 { 10 | didSet { 11 | hideValues = false 12 | hideD = false 13 | switch blinkMode { 14 | case 1: 15 | labelA.stringValue = "Delay" 16 | labelB.stringValue = "On Time" 17 | labelC.stringValue = "Group Count" 18 | labelD.stringValue = "Group Delay" 19 | case 2: 20 | labelA.stringValue = "Min (1-32)" 21 | labelB.stringValue = "Up Speed" 22 | labelC.stringValue = "Max (1-32)" 23 | labelD.stringValue = "Down Speed" 24 | case 3: 25 | labelA.stringValue = "Min (1-32)" 26 | labelB.stringValue = "Max (1-32)" 27 | labelC.stringValue = "Delay" 28 | hideD = true 29 | default: 30 | hideValues = true 31 | hideD = true 32 | } 33 | controller.setDocumentEdited(true) 34 | } 35 | } 36 | @objc dynamic var blinkValueA: Int16 = 0 37 | @objc dynamic var blinkValueB: Int16 = 0 38 | @objc dynamic var blinkValueC: Int16 = 0 39 | @objc dynamic var blinkValueD: Int16 = 0 40 | @objc dynamic var hideValues = true 41 | @objc dynamic var hideD = true 42 | @objc dynamic var hideDisabled = false 43 | private var blinkTicks = 0 44 | private var blinkCount = 0 45 | private var blinkOn = true 46 | 47 | override func nextFrame() { 48 | guard enabled else { 49 | return 50 | } 51 | switch blinkMode { 52 | case 1: 53 | // Square-wave 54 | // blinkValueA = delay between blinks 55 | // blinkValueB = light on-time 56 | // blinkValueC = number of blinks in a group 57 | // blinkValueD = delay between groups 58 | blinkTicks += 1 59 | alpha = 0 60 | if blinkCount < blinkValueC { 61 | if blinkOn { 62 | alpha = 1 63 | if blinkTicks >= blinkValueB { 64 | blinkOn = false 65 | blinkTicks = 0 66 | } 67 | } else { 68 | if blinkTicks >= blinkValueA { 69 | blinkOn = true 70 | blinkTicks = 0 71 | blinkCount += 1 72 | } 73 | } 74 | } 75 | if blinkCount >= blinkValueC && blinkTicks >= blinkValueD { 76 | blinkTicks = 0 77 | blinkCount = 0 78 | } 79 | case 2: 80 | // Triangle-wave 81 | // blinkValueA = minimum intensity (1-32) 82 | // blinkValueB = intensity increase per frame, x100 83 | // blinkValueC = maximum intensity (1-32) 84 | // blinkValueD = intensity decrease per frame, x100 85 | if blinkOn { 86 | if alpha < CGFloat(min(blinkValueC, 32)) * Self.TransparencyStep { 87 | alpha += CGFloat(blinkValueB)/100 * Self.TransparencyStep 88 | } else { 89 | blinkOn = false 90 | } 91 | } else { 92 | if alpha > CGFloat(max(blinkValueA, 0)) * Self.TransparencyStep { 93 | alpha -= CGFloat(blinkValueD)/100 * Self.TransparencyStep 94 | } else { 95 | blinkOn = true 96 | } 97 | } 98 | case 3: 99 | // Random pulsing 100 | // blinkValueA = minimum intensity (1-32) 101 | // blinkValueB = maximum intensity (1-32) 102 | // blinkValueC = delay between intensity changes 103 | blinkTicks += 1 104 | if blinkTicks >= blinkValueC { 105 | // Allow min/max to be swapped if necessary to form a valid range 106 | let range = (min(blinkValueA, blinkValueB)...max(blinkValueA, blinkValueB)).clamped(to: 0...32) 107 | alpha = CGFloat(Int16.random(in: range)) * Self.TransparencyStep 108 | blinkTicks = 0 109 | } 110 | default: 111 | alpha = 1 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/ShanView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ShanView: NSView { 4 | @IBOutlet weak var controller: ShanWindowController! 5 | private var fillColor = NSColor.black 6 | private var borderColor = NSColor.gray 7 | public override func draw(_ dirtyRect: NSRect) { 8 | guard controller.currentFrame >= 0 else { 9 | return 10 | } 11 | 12 | NSGraphicsContext.current?.imageInterpolation = .none 13 | fillColor.setFill() 14 | dirtyRect.fill() 15 | for layer in controller.layers { 16 | layer.draw(dirtyRect) 17 | } 18 | 19 | // Calculate transform for exit points 20 | var transform = AffineTransform(translationByX: dirtyRect.midX, byY: dirtyRect.midY) 21 | let angle = CGFloat(controller.framesPerSet-controller.currentFrame)/CGFloat(controller.framesPerSet) * 360 22 | let compress = 91...269 ~= angle ? (x: controller.downCompressX, y: controller.downCompressY) : (x: controller.upCompressX, y: controller.upCompressY) 23 | transform.scale(x: compress.x > 0 ? compress.x/100 : 1, y: compress.y > 0 ? compress.y/100 : 1) 24 | transform.rotate(byDegrees: angle) 25 | for points in controller.pointLayers { 26 | points.draw(transform) 27 | } 28 | 29 | borderColor.setFill() 30 | dirtyRect.frame() 31 | } 32 | 33 | // Toggle black background on click 34 | override func mouseDown(with event: NSEvent) { 35 | self.borderColor = self.fillColor == .black ? .quaternaryLabelColor : .gray 36 | self.fillColor = self.fillColor == .black ? .gridColor : .black 37 | super.mouseDown(with: event) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/ShieldLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ShieldLayer: SpriteLayer { 4 | override var enabled: Bool { 5 | didSet { 6 | alpha = 1 7 | } 8 | } 9 | 10 | // This layer should be initially disabled 11 | override init() { 12 | super.init() 13 | self.enabled = false 14 | super.alpha = 0 15 | } 16 | 17 | override func nextFrame() { 18 | if !enabled && alpha > 0 { 19 | alpha -= Self.TransparencyStep 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Shan Editor/WeaponLayer.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class WeaponLayer: ShieldLayer { 4 | @objc dynamic var decay: Int16 = 0 5 | 6 | override func nextFrame() { 7 | if !enabled && alpha > 0 { 8 | // Weapon decay roughly means drop this many transparency levels every 3 frames 9 | alpha -= CGFloat(decay) * Self.TransparencyStep / 3 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/SpobFilter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | /* 5 | * This filter rewrites the spob DefCount field to use the first 12 6 | * bits for the total count and the remaining 4 bits for the wave size. 7 | * The allows a template to interpret it using WB12 and WB04. 8 | * 9 | * From the EV Nova Bible, spöb DefCount: If you set this number 10 | * to be above 1000, ships will be launched from the planet or 11 | * station in waves. The last number in this field is the number of 12 | * ships in each wave, and the first 3-4 numbers (minus 1 from the 13 | * first digit) are the total number of ships in the planet's 14 | * fleet. For example, a value of 1082 would be four waves of two 15 | * ships for a total of eight. A value of 2005 would create waves 16 | * of five ships each, with 100 ships total in the planet's defense 17 | * fleet. 18 | */ 19 | 20 | class SpobFilter: TemplateFilter { 21 | static let supportedTypes = ["spöb"] 22 | static let name = "Defense Fleet Demangler" 23 | 24 | static func filter(data: Data, for resourceType: String) -> Data { 25 | guard data.count >= 32, (data[at: 30] != 0 || data[at: 31] != 0) else { 26 | return data 27 | } 28 | var defCount = Int16(bitPattern: UInt16(data[at: 30]) << 8 | UInt16(data[at: 31])) 29 | var waveSize: Int16 = 0 30 | if defCount < 0 { 31 | defCount = 0 32 | } else if defCount >= 1000 { 33 | defCount -= defCount >= 10000 ? 10000 : 1000 34 | waveSize = defCount % 10 35 | defCount /= 10 36 | } 37 | var data = data 38 | data[at: 30] = UInt8(defCount >> 4) 39 | data[at: 31] = UInt8((defCount & 0x0F) << 4 + waveSize) 40 | return data 41 | } 42 | 43 | static func unfilter(data: Data, for resourceType: String) -> Data { 44 | guard data.count >= 32, (data[at: 30] != 0 || data[at: 31] != 0) else { 45 | return data 46 | } 47 | var defCount = Int16(data[at: 30]) << 4 | Int16(data[at: 31]) >> 4 48 | var waveSize = data[at: 31] & 0x0F 49 | if waveSize > 0 || defCount >= 1000 { 50 | // Int16.max = 32767, which equals a total of 2276 in waves of 7 51 | // For waves of size 8 or 9, the max total is 2275 52 | waveSize = min(waveSize, 9) 53 | defCount = min(defCount, waveSize > 7 ? 2275 : 2276) 54 | defCount *= 10 55 | defCount += 10000 + Int16(waveSize) 56 | } 57 | var data = data 58 | data[at: 30] = UInt8(defCount >> 8) 59 | data[at: 31] = UInt8(defCount & 0xFF) 60 | return data 61 | } 62 | } 63 | 64 | fileprivate extension Data { 65 | @inline(__always) 66 | /// Access an element relative to the start index 67 | subscript(at offset: Int) -> UInt8 { 68 | get { 69 | return self[startIndex + offset] 70 | } 71 | set { 72 | self[startIndex + offset] = newValue 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Sprite Editor/Sprite.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | enum SpriteError: Error { 4 | case invalid 5 | case unsupported 6 | } 7 | 8 | protocol Sprite { 9 | var frameWidth: Int { get } 10 | var frameHeight: Int { get } 11 | var frameCount: Int { get } 12 | var data: Data { get } 13 | 14 | // Init for reading 15 | init(_ data: Data) throws 16 | 17 | func readFrame() throws -> NSBitmapImageRep 18 | 19 | func readSheet() throws -> NSBitmapImageRep 20 | } 21 | 22 | protocol WriteableSprite: Sprite { 23 | // Init for writing 24 | init(width: Int, height: Int, count: Int) 25 | 26 | func writeSheet(_ rep: NSImageRep, dither: Bool) -> [NSBitmapImageRep] 27 | 28 | func writeFrames(_ reps: [NSImageRep], dither: Bool) -> [NSBitmapImageRep] 29 | } 30 | 31 | // Utility functions helpful for implementers 32 | extension Sprite { 33 | func sheetGrid() -> (x: Int, y: Int) { 34 | var gridX = 6 35 | if frameCount <= gridX { 36 | gridX = frameCount 37 | } else { 38 | while frameCount % gridX != 0 { 39 | gridX += 1 40 | } 41 | } 42 | return (gridX, frameCount / gridX) 43 | } 44 | 45 | func newFrame(_ pixelsWide: Int? = nil, _ pixelsHigh: Int? = nil) -> NSBitmapImageRep { 46 | return NSBitmapImageRep(bitmapDataPlanes: nil, 47 | pixelsWide: pixelsWide ?? frameWidth, 48 | pixelsHigh: pixelsHigh ?? frameHeight, 49 | bitsPerSample: 8, 50 | samplesPerPixel: 4, 51 | hasAlpha: true, 52 | isPlanar: false, 53 | colorSpaceName: .deviceRGB, 54 | bytesPerRow: (pixelsWide ?? frameWidth) * 4, 55 | bitsPerPixel: 0)! 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/SteppingFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class SteppingFieldDelegate: NSObject, NSTextFieldDelegate { 4 | // Increment/decrement the value with up/down keypress 5 | func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { 6 | switch commandSelector { 7 | case #selector(NSControl.moveUp(_:)): 8 | control.integerValue += 1 9 | case #selector(NSControl.moveDown(_:)): 10 | control.integerValue -= 1 11 | case #selector(NSControl.cancelOperation(_:)): 12 | // Abort editing on escape 13 | control.abortEditing() 14 | return true 15 | default: 16 | return false 17 | } 18 | // To propagate the change to a bound value we first need to trigger a user change 19 | textView.insertText("", replacementRange: NSRange()) 20 | // Then commit the change 21 | textView.insertNewline(self) 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Plug-Ins/NovaTools/Templates.rsrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/Plug-Ins/NovaTools/Templates.rsrc -------------------------------------------------------------------------------- /Plug-Ins/Sound Editor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | 1 19 | NSPrincipalClass 20 | ${SWIFT_MODULE_NAME}.SoundWindowController 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResForge 2 | 3 | ![ResForge](https://github.com/andrews05/ResForge/raw/master/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_128.png) 4 | 5 | ResForge is a resource editor for macOS, capable of editing classic resource fork files and related formats. Based on [ResKnife](https://github.com/nickshanks/ResKnife) by Nicholas Shanks and Uli Kusterer, this derivative of the project has been rewritten for modern macOS systems. 6 | 7 | 8 | ## Installation 9 | 10 | Download the latest version of ResForge from the [Releases](https://github.com/andrews05/ResForge/releases) page. 11 | 12 | ResForge is compatible with macOS 10.15 or later and runs natively on both 64-bit Intel and Apple Silicon. 13 | 14 | 15 | ## Features 16 | 17 | * Hexadecimal editor, powered by [HexFiend](https://github.com/HexFiend/HexFiend). 18 | * Template editor, supporting a wide array of [field types](https://github.com/andrews05/ResForge/tree/master/ResForge/Template%20Editor#template-editor). 19 | * User-defined templates, loaded automatically from resource files in `~/Library/Application Support/ResForge/`. 20 | * Template-driven bulk data view, with CSV import/export. 21 | * Generic binary file editor, via the `Open with Template…` menu item. 22 | * Image editor, supporting 'PICT', 'PNG ', 'PNGf', 'cicn' & 'ppat' resources, plus view-only support for a variety of icons and other bitmaps. 23 | * Sound editor, supporting sampled 'snd ' resources. 24 | * Dialog editor, supporting 'DITL' resources (by Uli Kusterer) 25 | * Menu editor, supporting 'MENU', 'CMNU' & 'cmnu' resources (by Uli Kusterer) 26 | * Tools for EV Nova, including powerful templates for all types and a number of graphical editors. 27 | 28 | ### Supported File Formats 29 | 30 | * Macintosh resource format, in either resource fork or data fork. 31 | * Rez format, used by EV Nova. 32 | * Extended resource format, defined by [Graphite](https://github.com/TheDiamondProject/Graphite). 33 | * MacBinary encoded resource fork. 34 | * AppleSingle/AppleDouble encoded resource fork. 35 | 36 | 37 | ## Building 38 | 39 | To build ResForge yourself you will need to have Xcode 15 or later installed. 40 | 41 | Make sure to use the `--recurse-submodules` option when cloning the repository, or use `git submodule update --init` to initialise an existing copy. 42 | 43 | 44 | ## Built With 45 | 46 | * [HexFiend](https://github.com/HexFiend/HexFiend) - Powers the hexadecimal editor. 47 | * [CSV.swift](https://github.com/yaslab/CSV.swift) - Provides reading/writing of CSV files. 48 | * [swift-parsing](https://github.com/pointfreeco/swift-parsing) - Provides parsing of custom DSLs. 49 | 50 | 51 | ## Similar Projects 52 | 53 | * [resource_dasm](https://github.com/fuzziqersoftware/resource_dasm) - CLI tools for disassembling resource files. 54 | 55 | 56 | ## License 57 | 58 | Distributed under the MIT License. See `LICENSE` for more information. 59 | -------------------------------------------------------------------------------- /RFSupport/AbstractEditor.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | /// The abstract editor provides some default functionality for save handling. Do not extend this without also conforming to ResourceEditor. 4 | open class AbstractEditor: NSWindowController, NSWindowDelegate, NSMenuItemValidation { 5 | open override func loadWindow() { 6 | super.loadWindow() 7 | guard let window, let plug = window.windowController as? ResourceEditor else { 8 | return 9 | } 10 | // Use a short title for the title bar but include the document name in the Window menu 11 | window.title = plug.windowTitle 12 | if let docName = plug.resource.document?.displayName { 13 | NSApp.changeWindowsItem(window, title: "\(docName): \(window.title)", filename: false) 14 | } 15 | } 16 | 17 | open func windowShouldClose(_ sender: NSWindow) -> Bool { 18 | // Ensure any controls have ended editing 19 | if !sender.makeFirstResponder(nil) { 20 | return false 21 | } 22 | if sender.isDocumentEdited, let plug = sender.windowController as? ResourceEditor { 23 | if UserDefaults.standard.bool(forKey: "ConfirmChanges") { 24 | let alert = NSAlert() 25 | alert.messageText = NSLocalizedString("Do you want to save the changes you made to this resource?", comment: "") 26 | alert.addButton(withTitle: NSLocalizedString("Save Changes", comment: "")) 27 | alert.addButton(withTitle: NSLocalizedString("Discard", comment: "")) 28 | alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) 29 | alert.beginSheetModal(for: sender) { returnCode in 30 | switch returnCode { 31 | case .alertFirstButtonReturn: // keep 32 | plug.saveResource(alert) 33 | sender.close() 34 | case .alertSecondButtonReturn: // don't keep 35 | sender.close() 36 | default: 37 | break 38 | } 39 | } 40 | return false 41 | } 42 | plug.saveResource(sender) 43 | } 44 | return true 45 | } 46 | 47 | @IBAction open func saveDocument(_ sender: Any) { 48 | // Ensure any controls have ended editing, then save both the resource and the document 49 | if let editor = self as? ResourceEditor, editor.window?.makeFirstResponder(nil) != false { 50 | editor.saveResource(sender) 51 | editor.resource.document?.save(sender) 52 | } 53 | } 54 | 55 | @IBAction func exportResource(_ sender: Any) { 56 | self.exportResource(using: type(of: self) as? ExportProvider.Type) 57 | } 58 | 59 | @IBAction func exportRawResource(_ sender: Any) { 60 | self.exportResource(using: nil) 61 | } 62 | 63 | private func exportResource(using exporter: ExportProvider.Type?) { 64 | guard let resource = (self as? ResourceEditor)?.resource else { 65 | return 66 | } 67 | let panel = NSSavePanel() 68 | let filename = resource.filenameForExport(using: exporter) 69 | panel.nameFieldStringValue = "\(filename.name).\(filename.ext)" 70 | panel.isExtensionHidden = false 71 | if exporter != nil { 72 | panel.allowedFileTypes = [filename.ext] 73 | } 74 | panel.beginSheetModal(for: self.window!) { returnCode in 75 | if returnCode == .OK, let url = panel.url { 76 | do { 77 | if !resource.data.isEmpty, let exporter { 78 | try exporter.export(resource, to: url) 79 | } else { 80 | try resource.data.write(to: url) 81 | } 82 | } catch { 83 | self.presentError(error) 84 | } 85 | } 86 | } 87 | } 88 | 89 | open func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { 90 | switch menuItem.identifier?.rawValue { 91 | case "revertResource": 92 | menuItem.title = NSLocalizedString("Revert Resource", comment: "") 93 | fallthrough 94 | case "saveResource", "save": 95 | return self.window?.isDocumentEdited == true 96 | case "exportResource": 97 | return self is ExportProvider 98 | default: 99 | return true 100 | } 101 | } 102 | 103 | open func windowDidBecomeKey(_ notification: Notification) { 104 | if let createMenuTitle = (self as? ResourceEditor)?.createMenuTitle { 105 | let createItem = NSApp.mainMenu?.item(withTag: 3)?.submenu?.item(withTag: 0) 106 | createItem?.title = NSLocalizedString(createMenuTitle, comment: "") 107 | } 108 | } 109 | 110 | open func windowDidResignKey(_ notification: Notification) { 111 | if (self as? ResourceEditor)?.createMenuTitle != nil { 112 | let createItem = NSApp.mainMenu?.item(withTag: 3)?.submenu?.item(withTag: 0) 113 | createItem?.title = NSLocalizedString("New Resource…", comment: "") 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /RFSupport/BinaryDataReader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum BinaryDataReaderError: LocalizedError { 4 | case insufficientData 5 | case stringDecodeFailure 6 | public var errorDescription: String? { 7 | switch self { 8 | case .insufficientData: 9 | return NSLocalizedString("Insufficient data available.", comment: "") 10 | case .stringDecodeFailure: 11 | return NSLocalizedString("Unable to decode string in the requested encoding.", comment: "") 12 | } 13 | } 14 | } 15 | 16 | public class BinaryDataReader { 17 | public var data: Data 18 | public var bigEndian: Bool 19 | private(set) public var position: Int 20 | public var bytesRead: Int { 21 | position - data.startIndex 22 | } 23 | public var bytesRemaining: Int { 24 | data.endIndex - position 25 | } 26 | 27 | private var posStack = [Int]() 28 | 29 | public init(_ data: Data, bigEndian: Bool = true) { 30 | self.data = data 31 | self.position = data.startIndex 32 | self.bigEndian = bigEndian 33 | } 34 | 35 | @inline(__always) 36 | public func advance(_ count: Int) throws { 37 | guard count <= bytesRemaining else { 38 | throw BinaryDataReaderError.insufficientData 39 | } 40 | position += count 41 | } 42 | 43 | public func setPosition(_ position: Int) throws { 44 | let position = data.startIndex + position 45 | guard position <= data.endIndex else { 46 | throw BinaryDataReaderError.insufficientData 47 | } 48 | self.position = position 49 | } 50 | 51 | public func pushPosition(_ position: Int) throws { 52 | posStack.append(self.position) 53 | try self.setPosition(position) 54 | } 55 | 56 | public func popPosition() { 57 | assert(!posStack.isEmpty, "position stack is empty") 58 | self.position = posStack.removeLast() 59 | } 60 | 61 | public func read(bigEndian: Bool? = nil) throws -> T { 62 | let length = T.bitWidth / 8 63 | try self.advance(length) 64 | let val = data.withUnsafeBytes { 65 | $0.loadUnaligned(fromByteOffset: position-length-data.startIndex, as: T.self) 66 | } 67 | return bigEndian ?? self.bigEndian ? T(bigEndian: val) : T(littleEndian: val) 68 | } 69 | 70 | public func readData(length: Int) throws -> Data { 71 | try self.advance(length) 72 | return data[(position-length).. String { 76 | guard length != 0 else { 77 | return "" 78 | } 79 | let bytes = try self.readData(length: length) 80 | guard let string = String(data: bytes, encoding: encoding) else { 81 | throw BinaryDataReaderError.stringDecodeFailure 82 | } 83 | return string 84 | } 85 | 86 | public func readCString(encoding: String.Encoding = .utf8) throws -> String { 87 | guard let endIndex = data[position...].firstIndex(of: 0) else { 88 | throw BinaryDataReaderError.stringDecodeFailure 89 | } 90 | guard let string = String(bytes: data[position.. String { 99 | let length = Int(try self.read() as UInt8) 100 | return try self.readString(length: length, encoding: encoding) 101 | } 102 | 103 | public func readPString(encoding: String.Encoding = .macOSRoman, fixedSize: Int) throws -> String { 104 | let length = Int(try self.read() as UInt8) 105 | guard length < fixedSize else { 106 | throw BinaryDataReaderError.stringDecodeFailure 107 | } 108 | let string = try self.readString(length: length, encoding: encoding) 109 | try self.advance(fixedSize - length - 1) 110 | return string 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /RFSupport/BinaryDataWriter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum BinaryDataWriterError: Error { 4 | case notAStruct 5 | case stringEncodeFailure 6 | case outOfBounds 7 | } 8 | 9 | public class BinaryDataWriter { 10 | public var data: Data 11 | public var bigEndian: Bool 12 | public var bytesWritten: Int { data.count } 13 | 14 | public init(capacity: Int = 0, bigEndian: Bool = true) { 15 | self.data = Data(capacity: capacity) 16 | self.bigEndian = bigEndian 17 | } 18 | 19 | public func advance(_ count: Int) { 20 | data.append(Data(count: count)) 21 | } 22 | 23 | public func write(_ value: T, bigEndian: Bool? = nil) { 24 | let val = bigEndian ?? self.bigEndian ? value.bigEndian : value.littleEndian 25 | withUnsafePointer(to: val) { 26 | data.append(UnsafeBufferPointer(start: $0, count: 1)) 27 | } 28 | } 29 | 30 | public func write(_ value: T, at offset: Int, bigEndian: Bool? = nil) { 31 | let start = data.startIndex + offset 32 | let end = start + T.bitWidth/8 33 | let val = bigEndian ?? self.bigEndian ? value.bigEndian : value.littleEndian 34 | withUnsafeBytes(of: val) { 35 | data.replaceSubrange(start..> 24), 12 | UInt8(self >> 16 & 0xFF), 13 | UInt8(self >> 8 & 0xFF), 14 | UInt8(self & 0xFF) 15 | ] 16 | return String(bytes: bytes, encoding: .macOSRoman) ?? "" 17 | } 18 | 19 | /// Creates a new instance from four characters of a String, using macOSRoman encoding. 20 | init(fourCharString: String) { 21 | self = 0 22 | guard fourCharString != "" else { 23 | return 24 | } 25 | var bytes: [UInt8] = [0, 0, 0, 0] 26 | let max = Swift.min(fourCharString.count, 4) 27 | var used = 0 28 | var range = fourCharString.startIndex.. AnyObject? { 39 | var object: AnyObject? 40 | var errorString: NSString? 41 | guard getObjectValue(&object, for: string, errorDescription: &errorString) else { 42 | throw CocoaError(.keyValueValidation, userInfo: [NSLocalizedDescriptionKey: errorString as Any]) 43 | } 44 | return object 45 | } 46 | } 47 | 48 | // Conversions between hexadecimal Strings and Data 49 | public extension String { 50 | enum ExtendedEncoding { 51 | case hexadecimal 52 | } 53 | } 54 | 55 | public extension StringProtocol { 56 | /// Returns a Data containing a representation of the String encoded using a given encoding. 57 | func data(using encoding: String.ExtendedEncoding) -> Data? { 58 | guard count % 2 == 0 else { return nil } 59 | var newData = Data(capacity: count/2) 60 | for i in stride(from: 0, to: count, by: 2) { 61 | guard let byte = UInt8(self.dropFirst(i).prefix(2), radix: 16) else { 62 | return nil 63 | } 64 | newData.append(byte) 65 | } 66 | return newData 67 | } 68 | } 69 | 70 | public extension Data { 71 | /// Returns a hexadecimal String representation of the Data. 72 | var hexadecimal: String { 73 | return map { String(format: "%02X", $0) } 74 | .joined() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /RFSupport/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleVersion 18 | $(CURRENT_PROJECT_VERSION) 19 | 20 | 21 | -------------------------------------------------------------------------------- /RFSupport/MacRomanFormatter.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | public class MacRomanFormatter: Formatter { 4 | @IBInspectable public var stringLength: Int = 255 5 | @IBInspectable public var valueRequired: Bool = false 6 | @IBInspectable public var exactLengthRequired: Bool = false 7 | @IBInspectable public var convertLineEndings: Bool = false 8 | 9 | public convenience init(stringLength: Int = 255, valueRequired: Bool = false, exactLengthRequired: Bool = false, convertLineEndings: Bool = false) { 10 | self.init() 11 | self.stringLength = stringLength 12 | self.valueRequired = valueRequired 13 | self.exactLengthRequired = exactLengthRequired 14 | self.convertLineEndings = convertLineEndings 15 | } 16 | 17 | public override func string(for obj: Any?) -> String? { 18 | return obj as? String 19 | } 20 | 21 | public override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 22 | for string: String, 23 | errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 24 | if string.isEmpty { 25 | if valueRequired { 26 | error?.pointee = NSLocalizedString("The value must be not be blank.", comment: "") as NSString 27 | return false 28 | } 29 | } else if exactLengthRequired && string.count != stringLength { 30 | error?.pointee = String(format: NSLocalizedString("The value must be exactly %d characters.", comment: ""), stringLength) as NSString 31 | return false 32 | } 33 | if !string.canBeConverted(to: .macOSRoman) { 34 | error?.pointee = NSLocalizedString("The value contains invalid characters for Mac OS Roman encoding.", comment: "") as NSString 35 | return false 36 | } 37 | if convertLineEndings { 38 | // Convert LF to CR 39 | obj?.pointee = string.replacingOccurrences(of: "\n", with: "\r") as AnyObject 40 | } else { 41 | obj?.pointee = string as AnyObject 42 | } 43 | return true 44 | } 45 | 46 | public override func isPartialStringValid(_ partialStringPtr: AutoreleasingUnsafeMutablePointer, 47 | proposedSelectedRange proposedSelRangePtr: NSRangePointer?, 48 | originalString origString: String, 49 | originalSelectedRange origSelRange: NSRange, 50 | errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 51 | if partialStringPtr.pointee.length > stringLength { 52 | // If a range is selected then characters in that range will be removed so adjust the length accordingly 53 | var range = origSelRange 54 | range.length += max(stringLength - origString.count, 0) 55 | 56 | // Perform the replacement 57 | let insert = partialStringPtr.pointee.substring(with: range) 58 | partialStringPtr.pointee = (origString as NSString).replacingCharacters(in: origSelRange, with: insert) as NSString 59 | 60 | // Fix-up the proposed selection range 61 | proposedSelRangePtr?.pointee.location = range.location + range.length 62 | proposedSelRangePtr?.pointee.length = 0 63 | NSSound.beep() 64 | return false 65 | } 66 | 67 | return true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /RFSupport/ResForgePlugin.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | /// An editor provides a window for editing or viewing resources of the supported types. 4 | public protocol ResourceEditor: AbstractEditor { 5 | /// The list of resource types that this editor supports. 6 | static var supportedTypes: [String] { get } 7 | 8 | /// The resource that the editor is editing. 9 | var resource: Resource { get } 10 | 11 | /// The title of the editor's window. Default implementation provided. 12 | var windowTitle: String { get } 13 | 14 | /// The title of the "Create" menu item, if the editor implements the `createNewItem()` function. 15 | var createMenuTitle: String? { get } 16 | 17 | /// Initialise the editor with the resource to be edited and the editor manager. 18 | /// May return nil if the editor is unable to edit the resource. 19 | init?(resource: Resource, manager: RFEditorManager) 20 | 21 | // Implementers should declare these @IBAction (or @obj) 22 | func saveResource(_ sender: Any) 23 | func revertResource(_ sender: Any) 24 | } 25 | public extension ResourceEditor { 26 | var windowTitle: String { resource.defaultWindowTitle } 27 | var createMenuTitle: String? { nil } 28 | } 29 | 30 | /// A preview provider allows the document to display a grid view for a supported resource type. 31 | public protocol PreviewProvider { 32 | static var supportedTypes: [String] { get } 33 | 34 | /// Return the max thumbnail size for grid view. 35 | static func maxThumbnailSize(for resourceType: String) -> Int? 36 | 37 | /// Return an image representing the resource for use in grid view. 38 | static func image(for resource: Resource) -> NSImage? 39 | } 40 | public extension PreviewProvider { 41 | static func maxThumbnailSize(for resourceType: String) -> Int? { 42 | return nil 43 | } 44 | } 45 | 46 | /// An export provider allows control over the file written for a resource when exported. 47 | public protocol ExportProvider { 48 | static var supportedTypes: [String] { get } 49 | 50 | /// Return the filename extension for the resource type. 51 | static func filenameExtension(for resourceType: String) -> String 52 | 53 | /// Write a file representing the resource to the given url. 54 | static func export(_ resource: Resource, to url: URL) throws 55 | } 56 | 57 | /// A placeholder provider allows custom content to be shown as a placeholder in the list view when a resource has no name. 58 | public protocol PlaceholderProvider { 59 | static var supportedTypes: [String] { get } 60 | 61 | /// Return a placeholder name to show for a resource, or nil to use a default name. 62 | static func placeholderName(for resource: Resource) -> String? 63 | } 64 | 65 | /// A type icon provider allows specific icons to be shown in the type list. 66 | public protocol TypeIconProvider { 67 | /// Mapping of type codes to icon characters or symbol names. 68 | static var typeIcons: [String: String] { get } 69 | } 70 | 71 | /// A template filter can modify data to allow it to be interpreted by a template. 72 | public protocol TemplateFilter { 73 | static var supportedTypes: [String] { get } 74 | /// The name of the filter that will be shown in the template editor. 75 | static var name: String { get } 76 | 77 | /// Filter the data when reading into a template. 78 | static func filter(data: Data, for resourceType: String) throws -> Data 79 | 80 | /// Reverse the filter for writing data back to the resource. 81 | static func unfilter(data: Data, for resourceType: String) -> Data 82 | } 83 | 84 | /// The editor manager provides utility functions and is provided to editors on init. It should not be implemented by plugins. 85 | public protocol RFEditorManager: AnyObject { 86 | var document: NSDocument? { get } 87 | func open(resource: Resource) 88 | func open(resource: Resource, using editor: ResourceEditor.Type) 89 | func allResources(ofType: ResourceType, currentDocumentOnly: Bool) -> [Resource] 90 | func findResource(type: ResourceType, id: Int, currentDocumentOnly: Bool) -> Resource? 91 | func findResource(type: ResourceType, name: String, currentDocumentOnly: Bool) -> Resource? 92 | /// Open the resource creation modal with the given properties. Callback will be called if a resource was created with the same type that was requested. 93 | func createResource(type: ResourceType, id: Int?, name: String, callback: ((Resource) -> Void)?) 94 | } 95 | // Extension facilitates optional arguments for protocol functions. 96 | public extension RFEditorManager { 97 | func findResource(type: ResourceType, id: Int, currentDocumentOnly: Bool = false) -> Resource? { 98 | findResource(type: type, id: id, currentDocumentOnly: currentDocumentOnly) 99 | } 100 | func createResource(type: ResourceType, id: Int? = nil, name: String = "", callback: ((Resource) -> Void)? = nil) { 101 | createResource(type: type, id: id, name: name, callback: callback) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ResForge.pxd/QuickLook/Icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/QuickLook/Icon.webp -------------------------------------------------------------------------------- /ResForge.pxd/QuickLook/Thumbnail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/QuickLook/Thumbnail.webp -------------------------------------------------------------------------------- /ResForge.pxd/data/2551F86C-2CD7-42EE-A263-E298E5965398: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/data/2551F86C-2CD7-42EE-A263-E298E5965398 -------------------------------------------------------------------------------- /ResForge.pxd/data/2BE19F4E-9E80-422A-B514-B56283E29634: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/data/2BE19F4E-9E80-422A-B514-B56283E29634 -------------------------------------------------------------------------------- /ResForge.pxd/data/61E81C9A-2AB9-4996-8680-1076F4588BA4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/data/61E81C9A-2AB9-4996-8680-1076F4588BA4 -------------------------------------------------------------------------------- /ResForge.pxd/data/67C67F0B-2D50-4CBD-8ACB-B001F0079F23: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/data/67C67F0B-2D50-4CBD-8ACB-B001F0079F23 -------------------------------------------------------------------------------- /ResForge.pxd/data/CEDC7F92-2B01-46DA-923C-0DFC5197ADF2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/data/CEDC7F92-2B01-46DA-923C-0DFC5197ADF2 -------------------------------------------------------------------------------- /ResForge.pxd/data/F0060113-5EA7-4EB2-85AB-8EF8AA454EF8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/data/F0060113-5EA7-4EB2-85AB-8EF8AA454EF8 -------------------------------------------------------------------------------- /ResForge.pxd/metadata.info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge.pxd/metadata.info -------------------------------------------------------------------------------- /ResForge.xcodeproj/xcshareddata/xcschemes/ResForge (release).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ResForge.xcodeproj/xcshareddata/xcschemes/ResForge.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_128x128.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_16x16.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_256x256.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_32x32.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Document Image.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Document Image.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ResForge_16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "ResForge_16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "ResForge_32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "ResForge_32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "ResForge_128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "ResForge_128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "ResForge_256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "ResForge_256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "ResForge_512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "ResForge_512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_128.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_128@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_16.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_16@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_256.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_256@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_32.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_32@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_512.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/ResForge.appiconset/ResForge_512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/ResForge.appiconset/ResForge_512@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_128x128.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_16x16.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_256x256.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_32x32.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_512x512.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/Resource Document.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/Resource Document.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/rectangle.and.pencil.and.ellipsis.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rectangle.and.pencil.and.ellipsis.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "rectangle.and.pencil.and.ellipsis@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/rectangle.and.pencil.and.ellipsis.imageset/rectangle.and.pencil.and.ellipsis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/rectangle.and.pencil.and.ellipsis.imageset/rectangle.and.pencil.and.ellipsis.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/rectangle.and.pencil.and.ellipsis.imageset/rectangle.and.pencil.and.ellipsis@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/rectangle.and.pencil.and.ellipsis.imageset/rectangle.and.pencil.and.ellipsis@2x.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/square.and.pencil.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "square.and.pencil.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "square.and.pencil@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/square.and.pencil.imageset/square.and.pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/square.and.pencil.imageset/square.and.pencil.png -------------------------------------------------------------------------------- /ResForge/Assets.xcassets/square.and.pencil.imageset/square.and.pencil@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Assets.xcassets/square.and.pencil.imageset/square.and.pencil@2x.png -------------------------------------------------------------------------------- /ResForge/Classes/ApplicationDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | @main 5 | class ApplicationDelegate: NSObject, NSApplicationDelegate { 6 | static let githubURL = "https://github.com/andrews05/ResForge" 7 | 8 | private lazy var prefsController = PrefsController() 9 | 10 | override init() { 11 | NSApp.registerServicesMenuSendTypes([.string], returnTypes: [.string]) 12 | RFDefaults.register() 13 | } 14 | 15 | func applicationSupportsSecureRestorableState(_ application: NSApplication) -> Bool { 16 | true 17 | } 18 | 19 | func applicationDidFinishLaunching(_ notification: Notification) { 20 | // Load support resources and plugins 21 | NotificationCenter.default.addObserver(PluginRegistry.self, selector: #selector(PluginRegistry.bundleLoaded(_:)), name: Bundle.didLoadNotification, object: nil) 22 | SupportRegistry.scanForResources(in: Bundle.main) 23 | if let plugins = Bundle.main.builtInPlugInsURL { 24 | self.scanForPlugins(in: plugins) 25 | } 26 | let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .allDomainsMask) 27 | for url in appSupport { 28 | SupportRegistry.scanForResources(in: url.appendingPathComponent("ResForge")) 29 | } 30 | } 31 | 32 | func application(_ sender: NSApplication, openFile filename: String) -> Bool { 33 | // Abort open panel when opening a document by other means 34 | if sender.modalWindow is NSOpenPanel { 35 | sender.abortModal() 36 | } 37 | return false 38 | } 39 | 40 | func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { 41 | let launchAction = UserDefaults.standard.string(forKey: RFDefaults.launchAction) 42 | switch launchAction { 43 | case RFDefaults.LaunchAction.OpenUntitledFile.rawValue: 44 | return true 45 | case RFDefaults.LaunchAction.DisplayOpenPanel.rawValue: 46 | NSDocumentController.shared.openDocument(sender) 47 | return false 48 | default: 49 | return false 50 | } 51 | } 52 | 53 | @IBAction func showInfo(_ sender: Any) { 54 | let info = InfoWindowController.shared 55 | if info.window?.isKeyWindow == true { 56 | info.close() 57 | } else { 58 | info.showWindow(sender) 59 | } 60 | } 61 | 62 | @IBAction func showPrefs(_ sender: Any) { 63 | self.prefsController.showWindow(sender) 64 | } 65 | 66 | @IBAction func viewWebsite(_ sender: Any) { 67 | if let url = URL(string: Self.githubURL) { 68 | NSWorkspace.shared.open(url) 69 | } 70 | } 71 | 72 | private func scanForPlugins(in folder: URL) { 73 | let items: [URL] 74 | do { 75 | items = try FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) 76 | } catch { 77 | return 78 | } 79 | for item in items where item.pathExtension == "plugin" { 80 | guard let plugin = Bundle(url: item) else { 81 | continue 82 | } 83 | SupportRegistry.scanForResources(in: plugin) 84 | plugin.load() 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ResForge/Classes/ConflictResolver.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import OrderedCollections 3 | import RFSupport 4 | 5 | enum ConflictResolution { 6 | case unique 7 | case replace 8 | case skip 9 | } 10 | 11 | class ConflictResolver { 12 | private(set) var toAdd: [Resource] = [] 13 | private(set) var toRemove: [Resource] = [] 14 | private let document: ResourceDocument 15 | private let multiple: Bool 16 | private var resolution: ConflictResolution? 17 | private lazy var alert: NSAlert = { 18 | let alert = NSAlert() 19 | alert.informativeText = NSLocalizedString("Do you wish to assign the new resource a unique ID, replace the existing resource, or skip this resource?", comment: "") 20 | alert.addButton(withTitle: NSLocalizedString("Unique ID", comment: "")) 21 | alert.addButton(withTitle: NSLocalizedString("Replace", comment: "")) 22 | alert.addButton(withTitle: NSLocalizedString("Skip", comment: "")) 23 | alert.suppressionButton?.title = NSLocalizedString("Apply to all", comment: "") 24 | return alert 25 | }() 26 | 27 | init(document: ResourceDocument, multiple: Bool) { 28 | self.document = document 29 | self.multiple = multiple 30 | } 31 | 32 | func process(type: ResourceType, resources: [Resource]) { 33 | guard let existing = document.directory.resourceMap[type] else { 34 | toAdd.append(contentsOf: resources) 35 | return 36 | } 37 | // Keep an ordered mapping of ids to resources to allow us to quickly find both conflicts and unique ids 38 | var idMap = existing.reduce(into: OrderedDictionary()) { $0[$1.id] = $1 } 39 | for resource in resources { 40 | if let conflicted = idMap[resource.id] { 41 | switch resolution ?? self.getResolution(for: conflicted) { 42 | case .unique: 43 | var index = idMap.index(forKey: conflicted.id)! 44 | resource.id = document.directory.nextAvailableID(in: idMap.keys, startingAt: &index) 45 | idMap.updateValue(resource, forKey: resource.id, insertingAt: index) 46 | case .replace: 47 | toRemove.append(conflicted) 48 | case .skip: 49 | continue 50 | } 51 | } else { 52 | idMap.insert(key: resource.id, value: resource) { $0 < $1 } 53 | } 54 | toAdd.append(resource) 55 | } 56 | } 57 | 58 | private func getResolution(for conflicted: Resource) -> ConflictResolution { 59 | alert.messageText = String(format: NSLocalizedString("A resource of type ‘%@’ with ID %ld already exists.", comment: ""), conflicted.typeCode, conflicted.id) 60 | alert.showsSuppressionButton = multiple 61 | // TODO: Do this in a non-blocking way? 62 | alert.beginSheetModal(for: document.windowForSheet!, completionHandler: NSApp.stopModal(withCode:)) 63 | let modalResponse = NSApp.runModal(for: alert.window) 64 | let resolution: ConflictResolution 65 | switch modalResponse { 66 | case .alertFirstButtonReturn: 67 | resolution = .unique 68 | case .alertSecondButtonReturn: 69 | resolution = .replace 70 | default: 71 | resolution = .skip 72 | } 73 | if alert.suppressionButton?.state == .on { 74 | self.resolution = resolution 75 | } 76 | return resolution 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ResForge/Classes/ImportPanel.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class ImportPanel: NSObject, NSOpenSavePanelDelegate { 5 | @IBOutlet var accessoryView: NSView! 6 | @IBOutlet var typeSelect: NSPopUpButton! 7 | @IBOutlet weak var document: ResourceDocument! 8 | 9 | func show(callback: @escaping(URL, ResourceType) -> Void) { 10 | let type = document.dataSource.selectedType() 11 | let types = document.editorManager.allResources(ofType: .basicTemplate) 12 | .map(\.name).sorted(by: { $0.localizedStandardCompare($1) == .orderedAscending }) 13 | typeSelect.removeAllItems() 14 | typeSelect.addItems(withTitles: types) 15 | if !self.select(type: type?.code) { 16 | // Default to 'STR ' - the only standard type we currently have a TMPB for 17 | self.select(type: "STR ") 18 | } 19 | let panel = NSOpenPanel() 20 | panel.allowedFileTypes = ["csv"] 21 | panel.accessoryView = accessoryView 22 | panel.isAccessoryViewDisclosed = true 23 | panel.delegate = self 24 | panel.beginSheetModal(for: document.windowForSheet!) { modalResponse in 25 | if modalResponse == .OK, let url = panel.url, let typeCode = self.typeSelect.titleOfSelectedItem { 26 | callback(url, ResourceType(typeCode, type?.attributes ?? [:])) 27 | } 28 | } 29 | } 30 | 31 | func panelSelectionDidChange(_ sender: Any?) { 32 | guard let url = (sender as! NSOpenPanel).url else { 33 | return 34 | } 35 | self.select(type: url.deletingPathExtension().lastPathComponent) 36 | } 37 | 38 | @discardableResult private func select(type: String?) -> Bool { 39 | if let type, let item = typeSelect.item(withTitle: type) { 40 | typeSelect.select(item) 41 | return true 42 | } 43 | return false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ResForge/Classes/OpenPanelDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class OpenPanelDelegate: NSDocumentController, NSOpenSavePanelDelegate { 4 | @IBOutlet var accessoryView: NSView! 5 | @IBOutlet var forkSelect: NSPopUpButton! 6 | @objc private var forkIndex = 0 7 | // Flag indicating when a file is opened via the open panel so the document can use our selected fork 8 | private var useSelectedFork = false 9 | private let formatter = ByteCountFormatter() 10 | private static let forks = [nil, FileFork.data, FileFork.rsrc] 11 | override var defaultType: String? { 12 | ClassicFormat.typeName 13 | } 14 | 15 | func getSelectedFork() -> FileFork? { 16 | if !useSelectedFork { 17 | return nil 18 | } 19 | // The selected fork is being read, make sure we clear the flag for next time 20 | useSelectedFork = false 21 | return Self.forks[forkIndex] 22 | } 23 | 24 | override func runModalOpenPanel(_ openPanel: NSOpenPanel, forTypes types: [String]?) -> Int { 25 | openPanel.delegate = self 26 | openPanel.accessoryView = accessoryView 27 | openPanel.treatsFilePackagesAsDirectories = true 28 | openPanel.isAccessoryViewDisclosed = forkIndex != 0 29 | forkSelect.item(at: 1)?.title = FileFork.data.name 30 | forkSelect.item(at: 2)?.title = FileFork.rsrc.name 31 | 32 | let response = super.runModalOpenPanel(openPanel, forTypes: types) 33 | if response == NSApplication.ModalResponse.OK.rawValue { 34 | // We're opening a file from the open panel, set the flag 35 | useSelectedFork = true 36 | } 37 | return response 38 | } 39 | 40 | func panelSelectionDidChange(_ sender: Any?) { 41 | guard let url = (sender as! NSOpenPanel).url else { 42 | forkSelect.item(at: 1)?.title = FileFork.data.name 43 | forkSelect.item(at: 2)?.title = FileFork.rsrc.name 44 | return 45 | } 46 | // Show the fork sizes in the menu 47 | let values: URLResourceValues 48 | do { 49 | values = try url.resourceValues(forKeys: [.fileSizeKey, .totalFileSizeKey]) 50 | } catch { 51 | return 52 | } 53 | let dataSize = values.fileSize! 54 | let rsrcSize = values.totalFileSize! - values.fileSize! 55 | let dataString = dataSize > 0 ? formatter.string(fromByteCount: Int64(dataSize)) : "empty" 56 | let rsrcString = rsrcSize > 0 ? formatter.string(fromByteCount: Int64(rsrcSize)) : "empty" 57 | forkSelect.item(at: 1)?.title = "\(FileFork.data.name) (\(dataString))" 58 | forkSelect.item(at: 2)?.title = "\(FileFork.rsrc.name) (\(rsrcString))" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ResForge/Classes/PrefsController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class PrefsController: NSWindowController, NSWindowDelegate, NSTableViewDataSource { 4 | @IBOutlet var launchActions: NSView! 5 | @IBOutlet var favoriteTable: NSTableView! 6 | @IBOutlet var favoriteTypes: NSArrayController! 7 | 8 | override var windowNibName: NSNib.Name? { 9 | "PrefsWindow" 10 | } 11 | 12 | override func windowDidLoad() { 13 | if let launchAction = UserDefaults.standard.value(forKey: RFDefaults.launchAction) as? String, 14 | let tag = RFDefaults.LaunchAction.index(of: launchAction), 15 | let button = launchActions.viewWithTag(tag) as? NSButton { 16 | button.state = .on 17 | } 18 | window?.center() 19 | } 20 | 21 | func windowWillClose(_ notification: Notification) { 22 | window?.makeFirstResponder(nil) 23 | } 24 | 25 | @IBAction func setLaunchAction(_ sender: NSButton) { 26 | UserDefaults.standard.set(RFDefaults.LaunchAction.allCases[sender.tag].rawValue, forKey: RFDefaults.launchAction) 27 | } 28 | 29 | @IBAction func add(_ sender: Any) { 30 | window?.makeFirstResponder(nil) 31 | favoriteTypes.addObject("") 32 | favoriteTable.editColumn(0, row: favoriteTypes.selectionIndex, with: nil, select: true) 33 | } 34 | 35 | @IBAction func remove(_ sender: Any) { 36 | favoriteTable.abortEditing() 37 | favoriteTypes.remove(atArrangedObjectIndexes: favoriteTypes.selectionIndexes) 38 | } 39 | 40 | func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { 41 | (favoriteTypes.arrangedObjects as? [Any])?[row] 42 | } 43 | 44 | func tableView(_ tableView: NSTableView, setObjectValue object: Any?, for tableColumn: NSTableColumn?, row: Int) { 45 | favoriteTypes.remove(atArrangedObjectIndex: row) 46 | // Don't insert if invalid 47 | if let object = object as? String { 48 | // Detect duplicates 49 | if let idx = (favoriteTypes.arrangedObjects as? [String])?.firstIndex(of: object) { 50 | favoriteTypes.setSelectionIndex(idx) 51 | } else { 52 | favoriteTypes.insert(object, atArrangedObjectIndex: row) 53 | } 54 | } 55 | } 56 | } 57 | 58 | // MARK: - 59 | 60 | struct RFDefaults { 61 | static let confirmChanges = "ConfirmChanges" 62 | static let deleteResourceWarning = "DeleteResourceWarning" 63 | static let launchAction = "LaunchAction" 64 | static let showSidebar = "ShowSidebar" 65 | static let thumbnailSize = "ThumbnailSize" 66 | static let favoriteTypes = "FavoriteTypes" 67 | static let resourceNameInTemplate = "ResourceNameInTemplate" 68 | 69 | enum LaunchAction: String, CaseIterable { 70 | case None 71 | case OpenUntitledFile 72 | case DisplayOpenPanel 73 | } 74 | 75 | static func register() { 76 | UserDefaults.standard.register(defaults: [ 77 | confirmChanges: false, 78 | deleteResourceWarning: true, 79 | launchAction: LaunchAction.DisplayOpenPanel.rawValue, 80 | showSidebar: true, 81 | thumbnailSize: 100, 82 | resourceNameInTemplate: false, 83 | favoriteTypes: [ 84 | // Common types? 85 | "PICT", 86 | "snd ", 87 | "STR ", 88 | "STR#", 89 | "TMPL", 90 | // EV Nova types - these are annoying to type so keep them here as defaults 91 | "bööm", 92 | "chär", 93 | "cölr", 94 | "crön", 95 | "dësc", 96 | "düde", 97 | "flët", 98 | "gövt", 99 | "ïntf", 100 | "jünk", 101 | "mïsn", 102 | "nëbu", 103 | "oütf", 104 | "öops", 105 | "përs", 106 | "ränk", 107 | "rlëD", 108 | "röid", 109 | "shän", 110 | "shïp", 111 | "spïn", 112 | "spöb", 113 | "sÿst", 114 | "wëap", 115 | ] 116 | ]) 117 | } 118 | } 119 | 120 | extension RawRepresentable where Self: CaseIterable, RawValue: Equatable { 121 | /// Returns the index of a raw value in the enum. 122 | static func index(of rawValue: RawValue) -> AllCases.Index? { 123 | return allCases.firstIndex { $0.rawValue == rawValue } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /ResForge/Classes/SelectTemplateController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class SelectTemplateController: NSWindowController, NSTextFieldDelegate { 5 | @IBOutlet var typeList: NSComboBox! 6 | @IBOutlet var openButton: NSButton! 7 | private var templates: [String: Resource] = [:] 8 | 9 | override var windowNibName: NSNib.Name? { 10 | "SelectTemplate" 11 | } 12 | 13 | func show(_ document: ResourceDocument, type: ResourceType, complete: @escaping (Resource) -> Void) { 14 | _ = self.window 15 | let allTemplates = document.editorManager.allResources(ofType: .template) 16 | for template in allTemplates { 17 | if templates[template.name] == nil { 18 | templates[template.name] = template 19 | } 20 | } 21 | let sorted = templates.keys.sorted { 22 | $0.localizedCompare($1) == .orderedAscending 23 | } 24 | typeList.addItems(withObjectValues: sorted) 25 | 26 | if templates[type.code] != nil { 27 | typeList.stringValue = type.code 28 | openButton.isEnabled = true 29 | } 30 | document.windowForSheet?.beginSheet(self.window!) { modalResponse in 31 | if modalResponse == .OK, let template = self.templates[self.typeList.stringValue] { 32 | complete(template) 33 | } 34 | } 35 | } 36 | 37 | func controlTextDidChange(_ obj: Notification) { 38 | // The text must be one of the options in the list. 39 | // (A popup menu might seem more appropriate but we want the convenience of being able to type it in.) 40 | openButton.isEnabled = templates[typeList.stringValue] != nil 41 | } 42 | 43 | @IBAction func hide(_ sender: AnyObject) { 44 | self.window?.sheetParent?.endSheet(self.window!, returnCode: sender === openButton ? .OK : .cancel) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ResForge/Classes/StandardController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class StandardController: OutlineController { 5 | @IBAction func doubleClickItems(_ sender: Any) { 6 | // Ignore double-clicks in table header 7 | guard outlineView.clickedRow != -1 else { 8 | return 9 | } 10 | for item in outlineView.selectedItems where item is ResourceType { 11 | // Expand the type list 12 | outlineView.expandItem(item) 13 | } 14 | document.openResources(sender) 15 | } 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | // Default sort resources by id 20 | // Note: awakeFromNib is re-triggered each time a cell is created - be careful not to re-sort each time 21 | if outlineView.sortDescriptors.isEmpty, let descriptor = outlineView.outlineTableColumn?.sortDescriptorPrototype { 22 | outlineView.sortDescriptors = [descriptor] 23 | } 24 | } 25 | 26 | override func prepareView(type: ResourceType?) -> NSView { 27 | currentType = type 28 | outlineView.indentationPerLevel = type == nil ? 1 : 0 29 | outlineView.tableColumns[0].width = type == nil ? 76 : 66 30 | self.setSorter() 31 | return scrollView 32 | } 33 | 34 | override func updated(resource: Resource, oldIndex: Int?) { 35 | // Always return first responder to the outlineView, e.g. in case of inline editing 36 | document.dataSource.ignoreChanges = true 37 | outlineView.window?.makeFirstResponder(outlineView) 38 | document.dataSource.ignoreChanges = false 39 | 40 | let parent = currentType == nil ? resource.type : nil 41 | let newIndex = document.directory.filteredResources(type: resource.type).firstIndex(of: resource) 42 | self.updateRow(oldIndex: oldIndex, newIndex: newIndex, parent: parent) 43 | if outlineView.selectedItem as? Resource == resource { 44 | outlineView.scrollRowToVisible(outlineView.selectedRow) 45 | } 46 | outlineView.reloadItem(resource) 47 | } 48 | 49 | // MARK: - Delegate functions 50 | 51 | func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { 52 | let view: NSTableCellView 53 | if let resource = item as? Resource { 54 | view = outlineView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as! NSTableCellView 55 | switch tableColumn!.identifier.rawValue { 56 | case "id": 57 | view.textField?.integerValue = resource.id 58 | view.imageView?.image = resource.statusIcon() 59 | case "name": 60 | view.textField?.bind(.value, to: resource, withKeyPath: "name") 61 | view.textField?.placeholderString = resource.placeholderName() 62 | case "size": 63 | view.textField?.integerValue = resource.data.count 64 | default: 65 | return nil 66 | } 67 | return view 68 | } else if let type = item as? ResourceType { 69 | let identifier = "\(tableColumn!.identifier.rawValue)Group" 70 | view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(identifier), owner: self) as! NSTableCellView 71 | switch identifier { 72 | case "idGroup": 73 | view.textField?.stringValue = type.code 74 | case "nameGroup": 75 | view.textField?.stringValue = type.attributesDisplay 76 | case "sizeGroup": 77 | view.textField?.integerValue = document.directory.resourceMap[type]!.count 78 | default: 79 | return nil 80 | } 81 | return view 82 | } 83 | return nil 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ResForge/Classes/SupportRegistry.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class SupportRegistry { 5 | static let directory = SupportRegistry() 6 | 7 | private(set) var resourceMap: [ResourceType: [Resource]] = [:] 8 | // The support registry is where most templates are found and since the directory doesn't 9 | // change after app launch we can keep an index of resources by name for faster template lookups 10 | private var nameIndex: [ResourceType: [String: Resource]] = [:] 11 | 12 | func resources(ofType type: ResourceType) -> [Resource] { 13 | return resourceMap[type] ?? [] 14 | } 15 | 16 | func findResource(type: ResourceType, id: Int) -> Resource? { 17 | return resourceMap[type]?.first { $0.id == id } 18 | } 19 | 20 | func findResource(type: ResourceType, name: String) -> Resource? { 21 | if nameIndex[type] == nil { 22 | guard let resources = resourceMap[type] else { 23 | return nil 24 | } 25 | nameIndex[type] = resources.reduce(into: [String: Resource]()) { map, resource in 26 | // Make sure later resources do not override earlier ones 27 | if map[resource.name] == nil { 28 | map[resource.name] = resource 29 | } 30 | } 31 | } 32 | return nameIndex[type]?[name] 33 | } 34 | 35 | static func scanForResources(in folder: URL) { 36 | let items: [URL] 37 | do { 38 | items = try FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) 39 | } catch { 40 | return 41 | } 42 | for item in items.sorted(by: { $0.path.localizedStandardCompare($1.path) == .orderedAscending }) { 43 | Self.load(resourceFile: item) 44 | } 45 | } 46 | 47 | static func scanForResources(in bundle: Bundle) { 48 | guard let items = bundle.urls(forResourcesWithExtension: "rsrc", subdirectory: nil) else { 49 | return 50 | } 51 | for item in items { 52 | Self.load(resourceFile: item) 53 | } 54 | } 55 | 56 | private static func load(resourceFile: URL) { 57 | do { 58 | let data = try Data(contentsOf: resourceFile) 59 | let format = try ResourceFormat.from(data: data) 60 | let resourceMap = try format.read(data) 61 | // Files loaded later should have precendence over files loaded earlier. This means their 62 | // resources should come first in the master list. To achieve this we sort the type lists 63 | // by id, then prepend them to the master list. 64 | for (rType, resources) in resourceMap { 65 | let resources = resources.sorted { $0.id < $1.id } 66 | directory.resourceMap[rType, default: []].insert(contentsOf: resources, at: 0) 67 | } 68 | } catch {} 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ResForge/Formats/AppleDoubleFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class AppleDoubleFormat: AppleSingleFormat { 5 | override var name: String { NSLocalizedString("AppleDouble Archive", comment: "") } 6 | 7 | override class var signature: UInt32 { 0x00051607 } 8 | } 9 | -------------------------------------------------------------------------------- /ResForge/Formats/AppleSingleFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | // https://web.archive.org/web/20180311140826/http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf 5 | 6 | class AppleSingleFormat: ClassicFormat { 7 | override var name: String { NSLocalizedString("AppleSingle Archive", comment: "") } 8 | 9 | class var signature: UInt32 { 0x00051600 } 10 | static let version: UInt32 = 0x00020000 11 | static let fillerBytes = 16 12 | static let resourceForkID: UInt32 = 2 13 | 14 | private var entries: [(UInt32, Data)] = [] 15 | 16 | override func filenameExtension(for url: URL?) -> String? { 17 | // We can't create AppleSingle files, so Save As will default to classic format 18 | return ClassicFormat.defaultExtension 19 | } 20 | 21 | override func read(_ data: Data) throws -> ResourceMap { 22 | var resourceMap: ResourceMap = [:] 23 | let reader = BinaryDataReader(data) 24 | 25 | // Read and validate header 26 | let signature = try reader.read() as UInt32 27 | let version = try reader.read() as UInt32 28 | guard signature == Self.signature, 29 | version == Self.version 30 | else { 31 | throw CocoaError(.fileReadCorruptFile) 32 | } 33 | try reader.advance(Self.fillerBytes) 34 | 35 | // Read the entries 36 | let numEntries = try reader.read() as UInt16 37 | for _ in 0.. 0 { 48 | resourceMap = try super.read(entry) 49 | } 50 | reader.popPosition() 51 | } 52 | 53 | return resourceMap 54 | } 55 | 56 | override func write(_ resourceMap: ResourceMap) throws -> Data { 57 | let entryDescriptorLength = 12 58 | 59 | // Construct the resource fork 60 | let rsrcFork = try super.write(resourceMap) 61 | 62 | // Write header 63 | let writer = BinaryDataWriter() 64 | writer.write(Self.signature) 65 | writer.write(Self.version) 66 | writer.advance(Self.fillerBytes) 67 | 68 | // Write the entry descriptors 69 | let numEntries = entries.count + 1 70 | writer.write(UInt16(numEntries)) 71 | var offset = writer.bytesWritten + (numEntries * entryDescriptorLength) 72 | for (id, entry) in entries { 73 | writer.write(id) 74 | writer.write(UInt32(offset)) 75 | writer.write(UInt32(entry.count)) 76 | offset += entry.count 77 | } 78 | writer.write(Self.resourceForkID) 79 | writer.write(UInt32(offset)) 80 | writer.write(UInt32(rsrcFork.count)) 81 | 82 | // Write the entry data 83 | for (_, entry) in entries { 84 | writer.writeData(entry) 85 | } 86 | writer.writeData(rsrcFork) 87 | 88 | return writer.data 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ResForge/Formats/MacBinaryFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | // https://web.archive.org/web/20050305044255/http://www.lazerware.com/formats/macbinary/macbinary_iii.html 5 | 6 | class MacBinaryFormat: ClassicFormat { 7 | override var name: String { NSLocalizedString("MacBinary Archive", comment: "") } 8 | 9 | static let headerLength = 128 10 | static let forkLengthOffset = 83 11 | static let crcOffset = 124 12 | 13 | private var headerAndData = Data() 14 | 15 | override func filenameExtension(for url: URL?) -> String? { 16 | // We can't create MacBinary files, so Save As will default to classic format 17 | return ClassicFormat.defaultExtension 18 | } 19 | 20 | // MacBinary II and III are currently supported 21 | static func matches(data: Data) -> Bool { 22 | let versionOffset = 122 23 | let minimumVersionOffset = 123 24 | let v2version = 129 25 | let v3version = 130 26 | 27 | guard data.count >= Self.headerLength, 28 | data[0] == 0, 29 | data[74] == 0, 30 | data[82] == 0, 31 | (data[versionOffset] == v2version || data[versionOffset] == v3version), 32 | data[minimumVersionOffset] == v2version 33 | else { 34 | return false 35 | } 36 | 37 | return true 38 | } 39 | 40 | override func read(_ data: Data) throws -> ResourceMap { 41 | let reader = BinaryDataReader(data) 42 | 43 | // Validate the CRC 44 | try reader.setPosition(Self.crcOffset) 45 | let crc = try reader.read() as UInt16 46 | guard crc != 0, crc == self.crc16(data[0.. Data { 70 | // Construct the resource fork 71 | let rsrcFork = try super.write(resourceMap) 72 | 73 | // Write header and data fork 74 | let writer = BinaryDataWriter() 75 | writer.writeData(headerAndData) 76 | 77 | // Update the resource fork length 78 | writer.write(UInt32(rsrcFork.count), at: Self.forkLengthOffset + 4) 79 | 80 | // Update the CRC 81 | let crc = self.crc16(writer.data[0.. Int { 93 | let mod = size % 128 94 | return mod == 0 ? 0 : 128 - mod 95 | } 96 | 97 | // CRC-CCITT (XModem) 98 | private func crc16(_ data: Data) -> UInt16 { 99 | var x: UInt8 = 0 100 | var crc: UInt16 = 0 101 | for byte in data { 102 | x = UInt8(crc >> 8) ^ byte 103 | x ^= x >> 4 104 | crc = (crc << 8) ^ (UInt16(x) << 12) ^ (UInt16(x) << 5) ^ UInt16(x) 105 | } 106 | return crc 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ResForge/Formats/ResourceFileFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OrderedCollections 3 | import RFSupport 4 | 5 | typealias ResourceMap = OrderedDictionary 6 | 7 | protocol ResourceFileFormat { 8 | associatedtype IDType: FixedWidthInteger 9 | static var typeName: String { get } 10 | var name: String { get } 11 | var supportsResAttributes: Bool { get } 12 | var supportsTypeAttributes: Bool { get } 13 | 14 | func filenameExtension(for url: URL?) -> String? 15 | func read(_ data: Data) throws -> ResourceMap 16 | func write(_ resourceMap: ResourceMap) throws -> Data 17 | } 18 | 19 | // Implement some typical defaults and helpers for all formats 20 | extension ResourceFileFormat { 21 | typealias IDType = Int16 22 | var supportsResAttributes: Bool { false } 23 | var supportsTypeAttributes: Bool { false } 24 | 25 | func filenameExtension(for url: URL?) -> String? { 26 | return nil 27 | } 28 | 29 | static func isValid(id: Int) -> Bool { 30 | return Int(IDType.min)...Int(IDType.max) ~= id 31 | } 32 | func isValid(id: Int) -> Bool { 33 | return Self.isValid(id: id) 34 | } 35 | } 36 | 37 | // Convenience functions for format detection 38 | struct ResourceFormat { 39 | // We can only create new files of these types 40 | static let creatableTypes = [ 41 | ClassicFormat.typeName, 42 | RezFormat.typeName, 43 | ExtendedFormat.typeName, 44 | ] 45 | 46 | static func from(data: Data) throws -> any ResourceFileFormat { 47 | guard !data.isEmpty else { 48 | // Default to classic 49 | return ClassicFormat() 50 | } 51 | 52 | // Start by checking 4 byte signature 53 | let reader = BinaryDataReader(data) 54 | guard let signature = try? reader.read() as UInt32 else { 55 | throw CocoaError(.fileReadCorruptFile) 56 | } 57 | switch signature { 58 | case RezFormat.signature: 59 | return RezFormat() 60 | case ExtendedFormat.signature: 61 | return ExtendedFormat() 62 | case AppleSingleFormat.signature: 63 | return AppleSingleFormat() 64 | case AppleDoubleFormat.signature: 65 | return AppleDoubleFormat() 66 | case _ where MacBinaryFormat.matches(data: data): 67 | return MacBinaryFormat() 68 | default: 69 | // Fallback to classic 70 | return ClassicFormat() 71 | } 72 | } 73 | 74 | static func from(typeName: String) -> any ResourceFileFormat { 75 | switch typeName { 76 | case RezFormat.typeName: 77 | return RezFormat() 78 | case ExtendedFormat.typeName: 79 | return ExtendedFormat() 80 | default: 81 | return ClassicFormat() 82 | } 83 | } 84 | } 85 | 86 | enum ResourceFormatError: LocalizedError { 87 | case invalidID(Int) 88 | case typeAttributesNotSupported 89 | case fileTooBig 90 | case valueOverflow 91 | 92 | var failureReason: String? { 93 | switch self { 94 | case let .invalidID(id): 95 | return String(format: NSLocalizedString("The ID %ld is out of range for this file format.", comment: ""), id) 96 | case .typeAttributesNotSupported: 97 | return NSLocalizedString("Type attributes are not compatible with this file format.", comment: "") 98 | case .fileTooBig: 99 | return NSLocalizedString("The maximum file size of this format was exceeded.", comment: "") 100 | case .valueOverflow: 101 | return NSLocalizedString("An internal limit of this file format was exceeded.", comment: "") 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ResForge/Resources/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2821 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw12240\paperh15840\vieww11520\viewh8400\viewkind0 6 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 7 | 8 | \f0\b\fs22 \cf0 Developed By\ 9 | 10 | \f1\b0 Andrew Simmonds\ 11 | \ 12 | 13 | \f0\b Contributions By\ 14 | 15 | \f1\b0 Uli Kusterer\ 16 | Ben Dyer\ 17 | \ 18 | 19 | \f0\b Technologies Used 20 | \f1\b0 \ 21 | {\field{\*\fldinst{HYPERLINK "https://github.com/HexFiend/HexFiend"}}{\fldrslt HexFiend}} - Copyright \'a9 2005-2020 Peter Ammon\ 22 | {\field{\*\fldinst{HYPERLINK "https://github.com/yaslab/CSV.swift"}}{\fldrslt CSV.swift}} - Copyright \'a9 2016 Yasuhiro Hatta\ 23 | {\field{\*\fldinst{HYPERLINK "https://github.com/pointfreeco/swift-parsing"}}{\fldrslt swift-parsing}} - Copyright \'a9 2020 Point-Free Inc. 24 | \f0\b \ 25 | \ 26 | 27 | \f1\b0 ResForge is based upon the {\field{\*\fldinst{HYPERLINK "https://github.com/nickshanks/ResKnife"}}{\fldrslt ResKnife}} program by Nicholas Shanks and Uli Kusterer.} -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementAWRD.swift: -------------------------------------------------------------------------------- 1 | import RFSupport 2 | 3 | // Implements AWRD, ALNG, AL08, AL16 4 | class ElementAWRD: BaseElement { 5 | let alignment: Int 6 | 7 | required init(type: String, label: String) { 8 | switch type { 9 | case "AWRD": 10 | alignment = 2 11 | case "ALNG": 12 | alignment = 4 13 | default: 14 | alignment = Int(type.suffix(2))! 15 | } 16 | super.init(type: type, label: label) 17 | visible = false 18 | } 19 | 20 | private func align(_ pos: Int) -> Int { 21 | // Note: Swift % does not work as expected with negative values 22 | let m = pos % alignment 23 | return m == 0 ? 0 : alignment - m 24 | } 25 | 26 | override func readData(from reader: BinaryDataReader) throws { 27 | try reader.advance(self.align(reader.bytesRead)) 28 | } 29 | 30 | override func writeData(to writer: BinaryDataWriter) { 31 | writer.advance(self.align(writer.bytesWritten)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBBIT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements BBIT/BBnn/BFnn, WBIT/WBnn/WFnn, LBIT/LBnn/LFnn, QBIT/QBnn/QFnn 5 | class ElementBBIT: RangedElement { 6 | @objc private var value: UInt { 7 | get { UInt(tValue) } 8 | set { tValue = T(newValue) } 9 | } 10 | private var bits = 1 11 | private var position = 0 12 | private var bitList: [ElementBBIT] = [] 13 | private var first = true 14 | 15 | required init?(type: String, label: String) { 16 | if !type.hasSuffix("BIT") { 17 | // XXnn - bit field or fill bits 18 | bits = Int(type.suffix(2))! 19 | guard bits <= T.bitWidth else { 20 | return nil 21 | } 22 | } 23 | super.init(type: type, label: label) 24 | visible = type.dropFirst().first != "F" 25 | } 26 | 27 | override func configure() throws { 28 | if bits == 1 { 29 | // Single bit, configure like BOOL 30 | try ElementBOOL.readCases(for: self) 31 | } else { 32 | // Allow bitfields to configure cases 33 | try super.configure() 34 | } 35 | if !first { 36 | return 37 | } 38 | bitList.append(self) 39 | position = T.bitWidth - bits 40 | var pos = position 41 | var i = 0 42 | while pos > 0 { 43 | i += 1 44 | let next = parentList.peek(i) 45 | // Skip over cosmetic items 46 | if let next = next, ["CASE", "CASR", "DVDR", "RREF", "PACK"].contains(next.type) { 47 | continue 48 | } 49 | guard let bbit = next as? ElementBBIT else { 50 | throw TemplateError.invalidStructure(self, NSLocalizedString("Not enough bits in bit field.", comment: "")) 51 | } 52 | if bbit.bits > pos { 53 | throw TemplateError.invalidStructure(bbit, NSLocalizedString("Too many bits in bit field.", comment: "")) 54 | } 55 | pos -= bbit.bits 56 | bbit.position = pos 57 | bbit.first = false 58 | bitList.append(bbit) 59 | } 60 | } 61 | 62 | override func configure(view: NSView) { 63 | if bits == 1 { 64 | ElementBOOL.configure(view: view, for: self) 65 | } else { 66 | super.configure(view: view) 67 | if let field = view.subviews.first as? NSTextField { 68 | field.placeholderString = "\(bits) bits" 69 | } 70 | } 71 | } 72 | 73 | override func readData(from reader: BinaryDataReader) throws { 74 | if first { 75 | let completeValue = try reader.read() as T 76 | for bbit in bitList { 77 | bbit.tValue = (completeValue >> bbit.position) & ((1 << bbit.bits) - 1) 78 | } 79 | } 80 | } 81 | 82 | override func writeData(to writer: BinaryDataWriter) { 83 | if first { 84 | var completeValue: T = 0 85 | for bbit in bitList { 86 | completeValue |= T(bbit.tValue << bbit.position) 87 | } 88 | writer.write(completeValue) 89 | } 90 | } 91 | 92 | override var formatter: Formatter { 93 | self.sharedFormatter("UINT\(bits)") { 94 | let formatter = NumberFormatter() 95 | formatter.minimum = 0 96 | formatter.maximum = (1 << bits) - 1 as NSNumber 97 | formatter.allowsFloats = false 98 | formatter.nilSymbol = "\0" 99 | return formatter 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBFLG.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements BFLG, WFLG, LFLG 5 | class ElementBFLG: CasedElement { 6 | var tValue: T = 0 7 | @objc var value: Bool { 8 | get { tValue != 0 } 9 | set { tValue = newValue ? 1 : 0 } 10 | } 11 | 12 | override func configure() throws { 13 | try Self.readCases(for: self) 14 | } 15 | 16 | override func configure(view: NSView) { 17 | Self.configure(view: view, for: self) 18 | } 19 | 20 | override func readData(from reader: BinaryDataReader) throws { 21 | tValue = try reader.read() 22 | } 23 | 24 | override func writeData(to writer: BinaryDataWriter) { 25 | writer.write(tValue) 26 | } 27 | 28 | // MARK: - 29 | 30 | // Bit type elements will normally display as a checkbox but can be provided with CASEs to create radios instead. 31 | static func readCases(for element: CasedElement) throws { 32 | var valid = true 33 | while let caseEl = element.parentList.pop("CASE") as? ElementCASE { 34 | switch caseEl.displayValue.lowercased() { 35 | case "1", "on": 36 | caseEl.value = true 37 | case "0", "off": 38 | caseEl.value = false 39 | default: 40 | valid = false 41 | } 42 | if !valid || element.cases[caseEl.value] != nil { 43 | valid = false 44 | break 45 | } 46 | element.cases[caseEl.value] = caseEl 47 | } 48 | if !valid || (!element.cases.isEmpty && element.cases.count != 2) { 49 | throw TemplateError.invalidStructure(element, NSLocalizedString("CASE list must contain exactly two values: 1/On and 0/Off.", comment: "")) 50 | } 51 | element.width = element.cases.isEmpty ? 120 : 240 52 | } 53 | 54 | static func configure(view: NSView, for element: CasedElement) { 55 | if element.cases.isEmpty { 56 | view.addSubview(Self.createCheckbox(with: view.frame, for: element)) 57 | return 58 | } 59 | 60 | var frame = view.frame 61 | let width = element.width / 2 62 | frame.size.width = width - 4 63 | for case let (value as Bool, caseEl) in element.cases { 64 | let radio = NSButton(frame: frame) 65 | radio.setButtonType(.radio) 66 | radio.title = caseEl.displayLabel 67 | radio.action = #selector(TemplateEditor.itemValueUpdated(_:)) 68 | let options = value ? nil : [NSBindingOption.valueTransformerName: NSValueTransformerName.negateBooleanTransformerName] 69 | radio.bind(.value, to: element, withKeyPath: "value", options: options) 70 | view.addSubview(radio) 71 | frame.origin.x += width 72 | } 73 | } 74 | 75 | static func createCheckbox(with frame: NSRect, for element: BaseElement) -> NSButton { 76 | let checkbox = NSButton(frame: frame) 77 | checkbox.setButtonType(.switch) 78 | checkbox.bezelStyle = .regularSquare 79 | checkbox.title = element.metaValue ?? "\0" // Null character prevents clickable frame from taking up the whole width 80 | checkbox.action = #selector(TemplateEditor.itemValueUpdated(_:)) 81 | checkbox.bind(.value, to: element, withKeyPath: "value") 82 | if frame.width > 20 { 83 | checkbox.autoresizingMask = .width 84 | } 85 | return checkbox 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBHEX.swift: -------------------------------------------------------------------------------- 1 | import RFSupport 2 | 3 | // Implements BHEX, WHEX, LHEX, BSHX, WSHX, LSHX 4 | class ElementBHEX: ElementHEXD { 5 | private var skipLengthBytes = false 6 | private var lengthBytes = 0 7 | 8 | override func configure() throws { 9 | skipLengthBytes = type.hasSuffix("SHX") 10 | lengthBytes = T.bitWidth / 8 11 | width = 360 12 | } 13 | 14 | override func readData(from reader: BinaryDataReader) throws { 15 | length = Int(try reader.read() as T) 16 | if skipLengthBytes { 17 | guard length >= lengthBytes else { 18 | throw TemplateError.dataMismatch(self) 19 | } 20 | length -= lengthBytes 21 | } 22 | try super.readData(from: reader) 23 | } 24 | 25 | override func writeData(to writer: BinaryDataWriter) { 26 | var writeLength = length 27 | if skipLengthBytes { 28 | writeLength += lengthBytes 29 | } 30 | writer.write(T(writeLength)) 31 | super.writeData(to: writer) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBNDN.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements BNDN, LNDN, BIGE, LTLE 5 | class ElementBNDN: BaseElement, GroupElement { 6 | private let bigEndian: Bool 7 | 8 | required init(type: String, label: String) { 9 | bigEndian = type.first == "B" 10 | super.init(type: type, label: label) 11 | rowHeight = 16 12 | visible = !type.hasSuffix("NDN") 13 | } 14 | 15 | func configureGroup(view: NSTableCellView) { 16 | view.textField?.stringValue = displayLabel 17 | } 18 | 19 | override func readData(from reader: BinaryDataReader) throws { 20 | reader.bigEndian = bigEndian 21 | } 22 | 23 | override func writeData(to writer: BinaryDataWriter) { 24 | writer.bigEndian = bigEndian 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBOOL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ElementBOOL: ElementBFLG { 4 | @objc override var value: Bool { 5 | get { tValue >= 0x100 } 6 | set { tValue = newValue ? 0x100 : 0 } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBORV.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | /* 5 | * BORV is a Rezilla creation which is an OR combination of named (CASE) values 6 | * In Rezilla it is displayed as a multiple select popup menu 7 | * In ResForge we display it as a list of checkboxes (because we don't have enough checkbox types already!) 8 | * The main difference from BBITs is it allows a custom ordering of the bits (it also gives a slightly more compact display) 9 | * 10 | * Implements BORV, WORV, LORV, QORV 11 | */ 12 | class ElementBORV: ElementHBYT { 13 | override func configure() throws { 14 | width = 120 15 | self.defaultValue() 16 | try self.readCases() 17 | if cases.isEmpty { 18 | throw TemplateError.invalidStructure(self, NSLocalizedString("No ‘CASE’ elements found.", comment: "")) 19 | } 20 | for case let (value as T, caseEl) in cases { 21 | // Set the element to true/false for the checkbox state 22 | caseEl.value = (tValue & value) == value 23 | } 24 | rowHeight = Double(cases.count * 20) + 2 25 | } 26 | 27 | override func configure(view: NSView) { 28 | var frame = view.frame 29 | frame.origin.y += 1 30 | frame.size.height = 20 31 | for caseEl in cases.values { 32 | let checkbox = ElementBOOL.createCheckbox(with: frame, for: caseEl) 33 | checkbox.title = caseEl.displayLabel 34 | view.addSubview(checkbox) 35 | frame.origin.y += 20 36 | } 37 | } 38 | 39 | override func readData(from reader: BinaryDataReader) throws { 40 | tValue = try reader.read() 41 | for case let (value as T, caseEl) in cases { 42 | caseEl.value = (tValue & value) == value 43 | } 44 | } 45 | 46 | override func writeData(to writer: BinaryDataWriter) { 47 | tValue = 0 48 | for case let (value as T, caseEl) in cases { 49 | if caseEl.value as! Bool { 50 | tValue |= value 51 | } 52 | } 53 | writer.write(tValue) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementBSKP.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements BSKP, SKIP, LSKP, BSIZ, WSIZ, LSIZ 5 | class ElementBSKP: BaseElement, CollectionElement { 6 | let endType = "SKPE" 7 | private var subElements: ElementList! 8 | private var skipLengthBytes: Bool 9 | private var lengthBytes: Int 10 | @objc dynamic var value = -1 11 | 12 | required init(type: String, label: String) { 13 | skipLengthBytes = !type.hasSuffix("SIZ") 14 | lengthBytes = T.bitWidth / 8 15 | super.init(type: type, label: label) 16 | } 17 | 18 | override func configure() throws { 19 | subElements = try parentList.subList(for: self) 20 | try subElements.configure() 21 | } 22 | 23 | override func configure(view: NSView) { 24 | var frame = view.frame 25 | frame.origin.y += 3 26 | let textField = NSTextField(frame: frame) 27 | textField.drawsBackground = false 28 | textField.isBezeled = false 29 | textField.isEditable = false 30 | textField.isSelectable = true 31 | textField.bind(.value, to: self, withKeyPath: "value", options: [.valueTransformer: self]) 32 | view.addSubview(textField) 33 | } 34 | 35 | override func transformedValue(_ value: Any?) -> Any? { 36 | let value = value as! Int 37 | if value == -1 { 38 | return NSLocalizedString("(calculated on save)", comment: "") 39 | } else { 40 | return "\(value) " + NSLocalizedString("(recalculated on save)", comment: "") 41 | } 42 | } 43 | 44 | override func readData(from reader: BinaryDataReader) throws { 45 | value = Int(try reader.read() as T) 46 | var length = value 47 | if skipLengthBytes { 48 | guard length >= lengthBytes else { 49 | throw TemplateError.dataMismatch(self) 50 | } 51 | length -= lengthBytes 52 | } 53 | // Create a new reader for the skipped section of data 54 | // This ensures the sub elements cannot read past the end of the section 55 | let remainder = reader.bytesRemaining 56 | let data: Data 57 | if length > remainder { 58 | // Pad to expected length 59 | data = try reader.readData(length: remainder) + Data(count: length-remainder) 60 | } else { 61 | data = try reader.readData(length: length) 62 | } 63 | let subReader = BinaryDataReader(data, bigEndian: reader.bigEndian) 64 | try subElements.readData(from: subReader) 65 | if length > remainder { 66 | // Throw the expected error only after reading sub elements 67 | throw BinaryDataReaderError.insufficientData 68 | } 69 | } 70 | 71 | override func writeData(to writer: BinaryDataWriter) { 72 | let position = writer.bytesWritten 73 | writer.advance(lengthBytes) 74 | subElements.writeData(to: writer) 75 | var length = writer.bytesWritten - position 76 | if !skipLengthBytes { 77 | length -= lengthBytes 78 | } 79 | // Note: data corruption may occur if the length of the section exceeds the maximum size of the field 80 | writer.write(T(clamping: length), at: position) 81 | value = length 82 | } 83 | 84 | // MARK: - 85 | 86 | var subElementCount: Int { 87 | subElements.count 88 | } 89 | 90 | func subElement(at index: Int) -> BaseElement { 91 | return subElements.element(at: index) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementCASE.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class ElementCASE: BaseElement { 5 | @objc var value: AnyHashable! 6 | var displayValue: String { metaValue ?? displayLabel } 7 | 8 | convenience init(value: AnyHashable, displayLabel: String, displayValue: String) { 9 | self.init(type: "CASE", label: "") 10 | self.value = value 11 | self.displayLabel = displayLabel 12 | self.metaValue = displayValue 13 | } 14 | 15 | // For key elements, the case's description is used in the popup menu 16 | override var description: String { 17 | displayLabel 18 | } 19 | 20 | // Configure will only be called if the CASE is not associated to a supported element. 21 | // If this happens we will just show the label as a help tip below the previous element. 22 | override func configure() throws { 23 | displayLabel = "" 24 | rowHeight = 16 25 | } 26 | 27 | override func configure(view: NSView) { 28 | var frame = view.frame 29 | frame.origin.y -= 2 30 | let textField = NSTextField(frame: frame) 31 | textField.isBezeled = false 32 | textField.isEditable = false 33 | textField.isSelectable = true 34 | textField.stringValue = label 35 | textField.textColor = .secondaryLabelColor 36 | textField.drawsBackground = false 37 | view.addSubview(textField) 38 | } 39 | 40 | func configure(for element: FormattedElement) throws { 41 | do { 42 | value = try element.formatter.getObjectValue(for: displayValue) as? AnyHashable 43 | } catch let error { 44 | throw TemplateError.invalidStructure(self, error.localizedDescription) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementCHAR.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementCHAR: CasedElement { 5 | @objc private var value: UInt8 = 0 6 | 7 | override func readData(from reader: BinaryDataReader) throws { 8 | value = try reader.read() 9 | } 10 | 11 | override func writeData(to writer: BinaryDataWriter) { 12 | writer.write(value) 13 | } 14 | 15 | override var formatter: Formatter { 16 | self.sharedFormatter { CharFormatter() } 17 | } 18 | } 19 | 20 | /// Formatter for a single MacRoman character, with support for hex representation of non-printable characters. 21 | class CharFormatter: Formatter { 22 | private let hexFormatter = HexFormatter() 23 | private let macRomanFormatter = MacRomanFormatter(stringLength: 1, exactLengthRequired: true) 24 | 25 | public override func string(for obj: Any?) -> String? { 26 | guard let val = obj as? UInt8 else { 27 | return nil 28 | } 29 | switch val { 30 | case 0: 31 | return "" 32 | case 1...31: 33 | // Non-printable characters should be hex formatted 34 | return hexFormatter.string(for: val) 35 | default: 36 | // Otherwise render as MacRoman string 37 | return String(bytes: [val], encoding: .macOSRoman) 38 | } 39 | } 40 | 41 | public override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 42 | for string: String, 43 | errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 44 | do { 45 | // First try to interpret as MacRoman character 46 | let char = try macRomanFormatter.getObjectValue(for: string) as? String 47 | obj?.pointee = (char?.data(using: .macOSRoman)?.first ?? 0) as AnyObject 48 | } catch let err { 49 | do { 50 | // If MacRoman fails, try interpreting as hex 51 | obj?.pointee = try hexFormatter.getObjectValue(for: string) 52 | } catch _ { 53 | // If both fail, return the MacRoman error 54 | error?.pointee = err.localizedDescription as NSString? 55 | return false 56 | } 57 | } 58 | return true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementCOLR.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class ElementCOLR: BaseElement { 5 | var r: UInt16 = 0 6 | var g: UInt16 = 0 7 | var b: UInt16 = 0 8 | var mask: UInt = 0xFFFF 9 | 10 | @objc private var value: NSColor { 11 | get { 12 | return NSColor(red: CGFloat(r) / CGFloat(mask), 13 | green: CGFloat(g) / CGFloat(mask), 14 | blue: CGFloat(b) / CGFloat(mask), 15 | alpha: 1) 16 | } 17 | set { 18 | r = UInt16(round(newValue.redComponent * CGFloat(mask))) 19 | g = UInt16(round(newValue.greenComponent * CGFloat(mask))) 20 | b = UInt16(round(newValue.blueComponent * CGFloat(mask))) 21 | } 22 | } 23 | 24 | override func configure(view: NSView) { 25 | var frame = view.frame 26 | frame.size.width = width-4 27 | let well = NSColorWell(frame: frame) 28 | well.action = #selector(TemplateEditor.itemValueUpdated(_:)) 29 | well.bind(.value, to: self, withKeyPath: "value") 30 | view.addSubview(well) 31 | } 32 | 33 | override func readData(from reader: BinaryDataReader) throws { 34 | r = try reader.read() 35 | g = try reader.read() 36 | b = try reader.read() 37 | } 38 | 39 | override func writeData(to writer: BinaryDataWriter) { 40 | writer.write(r) 41 | writer.write(g) 42 | writer.write(b) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementDATE.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class ElementDATE: BaseElement { 5 | static let hfsToRef: Double = 2082844800+978307200 // Seconds between 1904 and 2001 6 | private var tValue: UInt32 = 0 7 | @objc private var value: Date { 8 | get { Date(timeIntervalSinceReferenceDate: Double(tValue) - Self.hfsToRef) } 9 | set { tValue = UInt32(newValue.timeIntervalSinceReferenceDate + Self.hfsToRef) } 10 | } 11 | 12 | override func configure() throws { 13 | width = 240 14 | value = Date() 15 | tValue += UInt32(TimeZone.current.secondsFromGMT()) 16 | } 17 | 18 | override func configure(view: NSView) { 19 | var frame = view.frame 20 | frame.origin.y -= 1 21 | frame.size.width = width-4 22 | frame.size.height = 24 23 | let picker = NSDatePicker(frame: frame) 24 | picker.minDate = Date(timeIntervalSinceReferenceDate: -Self.hfsToRef) 25 | picker.maxDate = Date(timeIntervalSinceReferenceDate: Double(UInt32.max)-Self.hfsToRef) 26 | picker.timeZone = TimeZone(secondsFromGMT: 0) 27 | picker.font = NSFont.systemFont(ofSize: 12) 28 | picker.drawsBackground = true 29 | picker.action = #selector(TemplateEditor.itemValueUpdated(_:)) 30 | picker.bind(.value, to: self, withKeyPath: "value") 31 | view.addSubview(picker) 32 | } 33 | 34 | override func readData(from reader: BinaryDataReader) throws { 35 | tValue = try reader.read() 36 | } 37 | 38 | override func writeData(to writer: BinaryDataWriter) { 39 | writer.write(tValue) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementDBYT.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | // Implements DBYT, DWRD, DLNG, DQWD, UBYT, UWRD, ULNG, UQWD 5 | class ElementDBYT: RangedElement { 6 | @objc private var value: NSNumber { 7 | get { tValue as! NSNumber } 8 | set { tValue = newValue as! T } 9 | } 10 | 11 | required init(type: String, label: String) { 12 | super.init(type: type, label: label) 13 | switch T.bitWidth/8 { 14 | case 4: 15 | width = 90 16 | case 8: 17 | width = 150 18 | default: 19 | break 20 | } 21 | } 22 | 23 | override func readData(from reader: BinaryDataReader) throws { 24 | tValue = try reader.read() 25 | } 26 | 27 | override func writeData(to writer: BinaryDataWriter) { 28 | writer.write(tValue) 29 | } 30 | 31 | override var formatter: Formatter { 32 | let key = T.isSigned ? "INT" : "UINT" 33 | return self.sharedFormatter("\(key)\(T.bitWidth)") { IntFormatter() } 34 | } 35 | } 36 | 37 | class IntFormatter: NumberFormatter, @unchecked Sendable { 38 | override init() { 39 | super.init() 40 | minimum = T.min as? NSNumber 41 | maximum = T.max as? NSNumber 42 | allowsFloats = false 43 | nilSymbol = "\0" 44 | } 45 | 46 | required init?(coder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementDOUB.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementDOUB: CasedElement { 5 | @objc private var value: Double = 0 6 | 7 | required init(type: String, label: String) { 8 | super.init(type: type, label: label) 9 | self.width = 180 10 | } 11 | 12 | override func readData(from reader: BinaryDataReader) throws { 13 | value = Double(bitPattern: try reader.read()) 14 | } 15 | 16 | override func writeData(to writer: BinaryDataWriter) { 17 | writer.write(value.bitPattern) 18 | } 19 | 20 | override var formatter: Formatter { 21 | self.sharedFormatter { 22 | let formatter = NumberFormatter() 23 | formatter.hasThousandSeparators = false 24 | formatter.numberStyle = .scientific 25 | formatter.minimum = 0 26 | formatter.maximum = Double.greatestFiniteMagnitude as NSNumber 27 | formatter.nilSymbol = "\0" 28 | return formatter 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementDVDR.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ElementDVDR: BaseElement, GroupElement { 4 | required init(type: String, label: String) { 5 | super.init(type: type, label: label) 6 | if label.isEmpty { 7 | rowHeight = 1 8 | } else { 9 | rowHeight = Double(label.components(separatedBy: "\n").count * 15) + 1 10 | } 11 | } 12 | 13 | func configureGroup(view: NSTableCellView) { 14 | view.textField?.stringValue = label 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementFBYT.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | // Implements FBYT, FWRD, FLNG, Fnnn 5 | class ElementFBYT: BaseElement { 6 | let length: Int 7 | var bytes: Data? 8 | 9 | required init(type: String, label: String) { 10 | switch type { 11 | case "FBYT": 12 | length = 1 13 | case "FWRD": 14 | length = 2 15 | case "FLNG": 16 | length = 4 17 | default: 18 | length = BaseElement.variableTypeValue(type) 19 | } 20 | super.init(type: type, label: label) 21 | visible = false 22 | } 23 | 24 | override func configure() { 25 | // If the metaValue contains hex bytes we will use this for the filler 26 | guard let metaValue, metaValue.starts(with: "0x") else { 27 | return 28 | } 29 | bytes = metaValue.dropFirst(2).data(using: .hexadecimal)?.prefix(length) 30 | } 31 | 32 | override func readData(from reader: BinaryDataReader) throws { 33 | try reader.advance(length) 34 | } 35 | 36 | override func writeData(to writer: BinaryDataWriter) { 37 | if let bytes { 38 | writer.writeData(bytes) 39 | writer.advance(length - bytes.count) 40 | } else { 41 | writer.advance(length) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementFCNT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ElementFCNT: BaseElement, GroupElement, CounterElement { 4 | var count: Int 5 | private let groupLabel: String 6 | 7 | required init(type: String, label: String) { 8 | // Read count from label - hex value denoted by leading '$' or '0x' 9 | let scanner = Scanner(string: label) 10 | if scanner.scanString("$") != nil { 11 | let value = scanner.scanInt32(representation: .hexadecimal) ?? 0 12 | count = Int(value) 13 | } else if label.starts(with: "0x") { 14 | let value = scanner.scanInt32(representation: .hexadecimal) ?? 0 15 | count = Int(value) 16 | } else { 17 | let value = scanner.scanInt32() ?? 0 18 | count = Int(abs(value)) 19 | } 20 | // Remove count from label 21 | groupLabel = label[scanner.currentIndex...].trimmingCharacters(in: .whitespaces) 22 | super.init(type: type, label: label) 23 | // Hide if no remaining label 24 | visible = !groupLabel.isEmpty 25 | } 26 | 27 | override func configure() throws { 28 | rowHeight = 16 29 | guard let lstc = parentList.next(ofType: "LSTC") as? ElementLSTB else { 30 | throw TemplateError.invalidStructure(self, NSLocalizedString("Following ‘LSTC’ element not found.", comment: "")) 31 | } 32 | lstc.counter = self 33 | lstc.visible = false 34 | lstc.fixedCount = true 35 | } 36 | 37 | func configureGroup(view: NSTableCellView) { 38 | view.textField?.stringValue = "\(groupLabel) = \(count)" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementFIXD.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementFIXD: CasedElement { 5 | static let fixed1 = Double(1 << 16) 6 | 7 | private var intValue: Int32 = 0 8 | @objc private var value: NSNumber { 9 | get { Double(intValue) / Self.fixed1 as NSNumber } 10 | set { intValue = Int32(round(newValue as! Double * Self.fixed1)) } 11 | } 12 | 13 | required init(type: String, label: String) { 14 | super.init(type: type, label: label) 15 | width = 90 16 | } 17 | 18 | override func readData(from reader: BinaryDataReader) throws { 19 | intValue = try reader.read() 20 | } 21 | 22 | override func writeData(to writer: BinaryDataWriter) { 23 | writer.write(intValue) 24 | } 25 | 26 | override var formatter: Formatter { 27 | self.sharedFormatter { 28 | let formatter = NumberFormatter() 29 | formatter.hasThousandSeparators = false 30 | formatter.numberStyle = .decimal 31 | formatter.maximumFractionDigits = 5 32 | formatter.minimum = Double(Int32.min) / Self.fixed1 as NSNumber 33 | formatter.maximum = Double(Int32.max) / Self.fixed1 as NSNumber 34 | formatter.nilSymbol = "\0" 35 | return formatter 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementHBYT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements HBYT, HWRD, HLNG, HQWD 5 | class ElementHBYT: CasedElement { 6 | var tValue: T = 0 7 | @objc private var value: NSNumber { 8 | get { tValue as! NSNumber } 9 | set { tValue = newValue as! T } 10 | } 11 | 12 | required init(type: String, label: String) { 13 | super.init(type: type, label: label) 14 | switch T.bitWidth/8 { 15 | case 4: 16 | width = 90 17 | case 8: 18 | width = 150 19 | default: 20 | break 21 | } 22 | } 23 | 24 | override func readData(from reader: BinaryDataReader) throws { 25 | tValue = try reader.read() 26 | } 27 | 28 | override func writeData(to writer: BinaryDataWriter) { 29 | writer.write(tValue) 30 | } 31 | 32 | override var formatter: Formatter { 33 | self.sharedFormatter("HEX\(T.bitWidth)") { HexFormatter() } 34 | } 35 | } 36 | 37 | class HexFormatter: Formatter { 38 | private let charCount: Int 39 | 40 | override init() { 41 | charCount = T.bitWidth/4 42 | super.init() 43 | } 44 | 45 | required init?(coder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | override func string(for obj: Any?) -> String? { 50 | if let obj = obj as? NSNumber { 51 | return String(format: "0x%0\(charCount)llX", obj.intValue) 52 | } 53 | return nil 54 | } 55 | 56 | override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 57 | for string: String, 58 | errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 59 | var string = string 60 | if string.first == "$" { 61 | string = String(string.dropFirst()) 62 | } 63 | let scanner = Scanner(string: string) 64 | let value = scanner.scanInt64(representation: .hexadecimal) ?? 0 65 | if !scanner.isAtEnd { 66 | error?.pointee = NSLocalizedString("The value is not a valid hex string.", comment: "") as NSString 67 | return false 68 | } 69 | if value > T.max { 70 | error?.pointee = NSLocalizedString("The value is too large.", comment: "") as NSString 71 | return false 72 | } 73 | obj?.pointee = value as NSNumber 74 | return true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementHEXD.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements HEXD, HEXS, Hnnn 5 | class ElementHEXD: BaseElement { 6 | var data: Data? 7 | var length = 0 8 | 9 | override func configure() throws { 10 | if type == "HEXD" || type == "HEXS" || type == "CODE" { 11 | guard self.isAtEnd() else { 12 | throw TemplateError.unboundedElement(self) 13 | } 14 | } else { 15 | // Hnnn 16 | length = BaseElement.variableTypeValue(type) 17 | data = Data(count: length) 18 | self.setRowHeight() 19 | } 20 | width = 360 21 | } 22 | 23 | override func configure(view: NSView) { 24 | var frame = view.frame 25 | frame.origin.y += 5 26 | frame.size.width = width - 4 27 | frame.size.height = CGFloat(rowHeight) - 9 28 | let textField = NSTextField(frame: frame) 29 | textField.isBezeled = false 30 | textField.isEditable = false 31 | textField.isSelectable = true 32 | textField.drawsBackground = false 33 | textField.font = NSFont.userFixedPitchFont(ofSize: 11) 34 | if let data { 35 | var count = 0 36 | textField.stringValue = data.map { 37 | count += 1 38 | return String(format: count.isMultiple(of: 4) ? "%02X " : "%02X", $0) 39 | } .joined() 40 | } 41 | view.addSubview(textField) 42 | } 43 | 44 | private func setRowHeight() { 45 | // 24 bytes per line, 13pt line height (minimum height 22) 46 | let lines = max(ceil(Double(length) / 24), 1) 47 | rowHeight = (lines * 13) + 9 48 | } 49 | 50 | override func readData(from reader: BinaryDataReader) throws { 51 | let remainder = reader.bytesRemaining 52 | if type == "HEXD" || type == "HEXS" || type == "CODE" { 53 | length = remainder 54 | } 55 | self.setRowHeight() 56 | if length > remainder { 57 | // Pad to expected length and throw error 58 | data = try reader.readData(length: remainder) + Data(count: length-remainder) 59 | throw BinaryDataReaderError.insufficientData 60 | } else { 61 | data = try reader.readData(length: length) 62 | } 63 | } 64 | 65 | override func writeData(to writer: BinaryDataWriter) { 66 | if let data { 67 | writer.writeData(data) 68 | } else { 69 | writer.advance(length) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementKBYT.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | // Implements KBYT, KWRD, KLNG, KQWD, KUBT, KUWD, KULG, KUQD 5 | class ElementKBYT: KeyElement { 6 | private var tValue: T = 0 7 | @objc private var value: NSNumber { 8 | get { tValue as! NSNumber } 9 | set { tValue = newValue as! T } 10 | } 11 | 12 | override func readData(from reader: BinaryDataReader) throws { 13 | tValue = try reader.read() 14 | self.setCase(value) 15 | } 16 | 17 | override func writeData(to writer: BinaryDataWriter) { 18 | writer.write(tValue) 19 | } 20 | 21 | override var formatter: Formatter { 22 | let key = T.isSigned ? "INT" : "UINT" 23 | return self.sharedFormatter("\(key)\(T.bitWidth)") { IntFormatter() } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementKCHR.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementKCHR: KeyElement { 5 | private var tValue: UInt8 = 0 6 | @objc private var value: String { 7 | get { 8 | tValue == 0 ? "" : String(bytes: [tValue], encoding: .macOSRoman)! 9 | } 10 | set { 11 | tValue = newValue.data(using: .macOSRoman)?.first ?? 0 12 | } 13 | } 14 | 15 | override func readData(from reader: BinaryDataReader) throws { 16 | tValue = try reader.read() 17 | self.setCase(value) 18 | } 19 | 20 | override func writeData(to writer: BinaryDataWriter) { 21 | writer.write(tValue) 22 | } 23 | 24 | override var formatter: Formatter { 25 | self.sharedFormatter("CHAR") { MacRomanFormatter(stringLength: 1, exactLengthRequired: true) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementKEYB.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementKEYB: BaseElement, CollectionElement { 5 | let endType = "KEYE" 6 | var subElements: ElementList! 7 | 8 | required init(type: String, label: String) { 9 | super.init(type: type, label: label) 10 | visible = false 11 | } 12 | 13 | override func copy() -> Self { 14 | let element = (super.copy() as BaseElement) as! Self 15 | element.subElements = try? subElements?.copy() 16 | return element 17 | } 18 | 19 | override func configure() throws { 20 | guard subElements != nil else { 21 | throw TemplateError.invalidStructure(self, NSLocalizedString("Not associated to a key element.", comment: "")) 22 | } 23 | } 24 | 25 | override func readData(from reader: BinaryDataReader) throws { 26 | try subElements.readData(from: reader) 27 | } 28 | 29 | override func writeData(to writer: BinaryDataWriter) { 30 | subElements.writeData(to: writer) 31 | } 32 | 33 | var subElementCount: Int { 34 | subElements.count 35 | } 36 | 37 | func subElement(at index: Int) -> BaseElement { 38 | return subElements.element(at: index) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementKHBT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | // Implements KHBT, KHWD, KHLG, KHQD 4 | class ElementKHBT: ElementKBYT { 5 | override var formatter: Formatter { 6 | self.sharedFormatter("HEX\(T.bitWidth)") { HexFormatter() } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementKRID.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class ElementKRID: KeyElement, GroupElement { 4 | override func configure() throws { 5 | rowHeight = 16 6 | // Read CASEs 7 | try self.readCases() 8 | guard let caseEl = cases[parentList.controller.resource.id] else { 9 | throw TemplateError.invalidStructure(self, NSLocalizedString("No ‘CASE’ for this resource id.", comment: "")) 10 | } 11 | // Read KEYBs 12 | while let keyB = parentList.pop("KEYB") as? ElementKEYB { 13 | keyB.subElements = try parentList.subList(for: keyB) 14 | let vals = keyB.label.components(separatedBy: ",") 15 | if vals.contains(caseEl.displayValue) { 16 | currentSection = keyB 17 | } 18 | } 19 | guard currentSection != nil else { 20 | throw TemplateError.invalidStructure(self, NSLocalizedString("No ‘KEYB’ for this resource id.", comment: "")) 21 | } 22 | currentSection.parentList = parentList 23 | parentList.insert(currentSection) 24 | try currentSection.subElements.configure() 25 | displayLabel += ": \(caseEl.displayLabel)" 26 | } 27 | 28 | func configureGroup(view: NSTableCellView) { 29 | view.textField?.stringValue = displayLabel 30 | } 31 | 32 | override var formatter: Formatter { 33 | self.sharedFormatter("INT16") { IntFormatter() } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementKTYP.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementKTYP: KeyElement { 5 | private var tValue: UInt32 = 0 6 | @objc private var value: String { 7 | get { tValue.fourCharString } 8 | set { tValue = FourCharCode(fourCharString: newValue) } 9 | } 10 | 11 | override func readData(from reader: BinaryDataReader) throws { 12 | tValue = try reader.read() 13 | self.setCase(value) 14 | } 15 | 16 | override func writeData(to writer: BinaryDataWriter) { 17 | writer.write(tValue) 18 | } 19 | 20 | override var formatter: Formatter { 21 | self.sharedFormatter("TNAM") { MacRomanFormatter(stringLength: 4, exactLengthRequired: true) } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementOCNT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | protocol CounterElement where Self: BaseElement { 5 | var count: Int { get set } 6 | } 7 | 8 | // Implements OCNT, ZCNT, BCNT, LCNT, LZCT 9 | class ElementOCNT: BaseElement, GroupElement, CounterElement { 10 | private var value: T = 0 11 | private weak var lstc: ElementLSTB! 12 | @objc var count: Int { 13 | get { 14 | Int(value) 15 | } 16 | set { 17 | value = T(newValue) 18 | } 19 | } 20 | 21 | override func configure() throws { 22 | rowHeight = 16 23 | lstc = parentList.next(ofType: "LSTC") as? ElementLSTB 24 | guard lstc != nil else { 25 | throw TemplateError.invalidStructure(self, NSLocalizedString("Following ‘LSTC’ element not found.", comment: "")) 26 | } 27 | lstc.counter = self 28 | } 29 | 30 | func configureGroup(view: NSTableCellView) { 31 | // Element will show as a group row - we need to combine the counter into the label 32 | view.textField?.bind(.value, to: self, withKeyPath: "count", options: [.valueTransformer: self]) 33 | } 34 | 35 | override func transformedValue(_ value: Any?) -> Any? { 36 | return "\(displayLabel) = \(value!)" 37 | } 38 | 39 | override func readData(from reader: BinaryDataReader) throws { 40 | lstc.reset() 41 | if T.isSigned { 42 | value = try reader.read() + 1 43 | guard value >= 0 else { 44 | throw TemplateError.dataMismatch(self) 45 | } 46 | } else { 47 | value = try reader.read() 48 | } 49 | for _ in 0.. 1 { 45 | if element.width > 180 { 46 | element.width = 180 47 | } 48 | if let element = element as? RangedController { 49 | element.popupWidth = element.popupWidth > 180 ? 180 : 120 50 | } 51 | } 52 | let prev = view.subviews.last 53 | element.configure(view: view) 54 | prev?.nextKeyView = view.subviews.last 55 | frame.origin.x += element.width 56 | frame.size.width -= element.width 57 | view.frame = frame 58 | } 59 | view.frame = orig 60 | width = frame.origin.x 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementPNT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class ElementPNT: BaseElement { 5 | @objc private var x: Int16 = 0 6 | @objc private var y: Int16 = 0 7 | 8 | override func configure() throws { 9 | self.width = 120 10 | } 11 | 12 | override func configure(view: NSView) { 13 | ElementRECT.configure(fields: ["x", "y"], in: view, for: self) 14 | } 15 | 16 | override func readData(from reader: BinaryDataReader) throws { 17 | x = try reader.read() 18 | y = try reader.read() 19 | } 20 | 21 | override func writeData(to writer: BinaryDataWriter) { 22 | writer.write(x) 23 | writer.write(y) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementPSTR.swift: -------------------------------------------------------------------------------- 1 | import RFSupport 2 | 3 | // Implements PSTR, BSTR, WSTR, LSTR, OSTR, ESTR, Pnnn 4 | class ElementPSTR: ElementCSTR { 5 | override func configurePadding() { 6 | maxLength = Int(T.max) 7 | switch type { 8 | case "PSTR", "BSTR", "WSTR", "LSTR": 9 | padding = .none 10 | case "OSTR": 11 | padding = .odd 12 | case "ESTR": 13 | padding = .even 14 | default: 15 | let nnn = BaseElement.variableTypeValue(type) 16 | padding = .fixed(nnn) 17 | maxLength = min(nnn-1, maxLength) 18 | } 19 | } 20 | 21 | override func readData(from reader: BinaryDataReader) throws { 22 | let length = min(Int(try reader.read() as T), maxLength) 23 | guard length <= reader.bytesRemaining else { 24 | throw TemplateError.dataMismatch(self) 25 | } 26 | 27 | value = try reader.readString(length: length, encoding: .macOSRoman) 28 | try reader.advance(padding.length(length + T.bitWidth/8)) 29 | } 30 | 31 | override func writeData(to writer: BinaryDataWriter) { 32 | if value.count > maxLength { 33 | value = String(value.prefix(maxLength)) 34 | } 35 | 36 | writer.write(T(value.count)) 37 | try? writer.writeString(value, encoding: .macOSRoman) 38 | writer.advance(padding.length(value.count + T.bitWidth/8)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementREAL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementREAL: CasedElement { 5 | @objc private var value: Float = 0 6 | 7 | required init(type: String, label: String) { 8 | super.init(type: type, label: label) 9 | self.width = 90 10 | } 11 | 12 | override func readData(from reader: BinaryDataReader) throws { 13 | value = Float(bitPattern: try reader.read()) 14 | } 15 | 16 | override func writeData(to writer: BinaryDataWriter) { 17 | writer.write(value.bitPattern) 18 | } 19 | 20 | override var formatter: Formatter { 21 | self.sharedFormatter { 22 | let formatter = NumberFormatter() 23 | formatter.hasThousandSeparators = false 24 | formatter.numberStyle = .scientific 25 | formatter.maximumSignificantDigits = 7 26 | formatter.minimum = 0 27 | formatter.maximum = Float.greatestFiniteMagnitude as NSNumber 28 | formatter.nilSymbol = "\0" 29 | return formatter 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementRECT.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | class ElementRECT: BaseElement { 5 | static let formatter = IntFormatter() 6 | @objc private var top: Int16 = 0 7 | @objc private var left: Int16 = 0 8 | @objc private var bottom: Int16 = 0 9 | @objc private var right: Int16 = 0 10 | 11 | override func configure() throws { 12 | width = 240 13 | } 14 | 15 | override func configure(view: NSView) { 16 | Self.configure(fields: ["top", "left", "bottom", "right"], in: view, for: self) 17 | } 18 | 19 | override func readData(from reader: BinaryDataReader) throws { 20 | top = try reader.read() 21 | left = try reader.read() 22 | bottom = try reader.read() 23 | right = try reader.read() 24 | } 25 | 26 | override func writeData(to writer: BinaryDataWriter) { 27 | writer.write(top) 28 | writer.write(left) 29 | writer.write(bottom) 30 | writer.write(right) 31 | } 32 | 33 | static func configure(fields: [String], in view: NSView, for element: BaseElement) { 34 | var frame = view.frame 35 | let width = element.width / CGFloat(fields.count) 36 | frame.size.width = width - 4 37 | for key in fields { 38 | let field = NSTextField(frame: frame) 39 | field.placeholderString = key 40 | field.formatter = Self.formatter 41 | field.delegate = element 42 | field.bind(.value, to: element, withKeyPath: key) 43 | view.addSubview(field) 44 | frame.origin.x += width 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementRNAM.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Allows editing the resource name within the template 5 | class ElementRNAM: CasedElement { 6 | @objc var value = "" 7 | 8 | override func configure() throws { 9 | // RNAM can only appear once and must be the first element 10 | guard parentList.parentElement == nil && parentList.element(at: 0) == self else { 11 | throw TemplateError.invalidStructure(self, NSLocalizedString("Must be first element in template.", comment: "")) 12 | } 13 | width = 240 14 | try super.configure() 15 | // In case data isn't read (resource is empty) we still need to ensure the value is populated 16 | value = parentList.controller.resource.name 17 | } 18 | 19 | override func configure(view: NSView) { 20 | super.configure(view: view) 21 | let textField = view.subviews.last as! NSTextField 22 | textField.placeholderString = NSLocalizedString("Untitled Resource", comment: "") 23 | } 24 | 25 | override func readData(from reader: BinaryDataReader) { 26 | value = parentList.controller.resource.name 27 | } 28 | 29 | override func writeData(to writer: BinaryDataWriter) { 30 | parentList.controller.resource.name = value 31 | } 32 | 33 | override var formatter: Formatter { 34 | self.sharedFormatter { 35 | MacRomanFormatter(stringLength: 255) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementRREF.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | /* 5 | * RREF is a static reference to another resource 6 | * The parameters are determined from the label, in the format "Display Label='TNAM' offset Button Label" 7 | * If offset is prefixed with # then the referenced id will equal the offset 8 | * Otherwise the referenced id will equal the current resource's id plus the offset 9 | */ 10 | class ElementRREF: BaseElement { 11 | private var resType = "" 12 | private var id = 0 13 | private var buttonLabel = "" 14 | 15 | override func configure() throws { 16 | guard let metaValue, 17 | case let scanner = Scanner(string: metaValue), 18 | scanner.scanString("'") != nil, 19 | let typeCode = scanner.scanUpToString("'"), 20 | typeCode.count == 4, 21 | scanner.scanString("'") != nil 22 | else { 23 | throw TemplateError.invalidStructure(self, NSLocalizedString("Could not determine resource type from label.", comment: "")) 24 | } 25 | resType = typeCode 26 | let isRelative = scanner.scanString("#") == nil 27 | id = scanner.scanInt() ?? 0 28 | if isRelative { 29 | id += parentList.controller.resource.id 30 | } 31 | if scanner.isAtEnd { 32 | buttonLabel = "\(resType) #\(id)" 33 | } else { 34 | buttonLabel = scanner.string[scanner.currentIndex...].trimmingCharacters(in: .whitespaces) 35 | } 36 | width = 120 37 | } 38 | 39 | override func configure(view: NSView) { 40 | var frame = view.frame 41 | frame.origin.y += 1 42 | frame.size.width = width - 4 43 | frame.size.height = 19 44 | let button = NSButton(frame: frame) 45 | button.bezelStyle = .inline 46 | button.title = buttonLabel 47 | button.font = .boldSystemFont(ofSize: 11) 48 | // Show add icon if resource does not exist, otherwise follow link icon 49 | let resource = parentList.controller.manager.findResource(type: .init(resType), id: id) 50 | button.image = NSImage(named: resource == nil ? NSImage.touchBarAddDetailTemplateName : NSImage.followLinkFreestandingTemplateName) 51 | if resource == nil { 52 | // The add icon isn't strictly supposed to be used outside of the touch bar - 53 | // It works fine on macOS 11 but for appropriate sizing on 10.14 we need to set the size explicitly (default is 18x30) 54 | button.image?.size = NSSize(width: 14, height: 24) 55 | } 56 | button.imagePosition = .imageRight 57 | button.target = self 58 | button.action = #selector(openResource(_:)) 59 | view.addSubview(button) 60 | } 61 | 62 | @IBAction func openResource(_ sender: Any) { 63 | parentList.controller.openOrCreateResource(typeCode: resType, id: id) { [weak self] resource, _ in 64 | // Update button image 65 | if let button = sender as? NSButton, resource.id == self?.id { 66 | button.image = NSImage(named: NSImage.followLinkFreestandingTemplateName) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementTNAM.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | class ElementTNAM: CasedElement { 5 | private var tValue: FourCharCode = 0 6 | // This is marked as dynamic so that RSID can bind to it and receive changes 7 | @objc dynamic private var value: String { 8 | get { tValue.fourCharString } 9 | set { tValue = FourCharCode(fourCharString: newValue) } 10 | } 11 | 12 | override func readData(from reader: BinaryDataReader) throws { 13 | tValue = try reader.read() 14 | value = String(value) // Trigger binding update 15 | } 16 | 17 | override func writeData(to writer: BinaryDataWriter) { 18 | writer.write(tValue) 19 | } 20 | 21 | override var formatter: Formatter { 22 | self.sharedFormatter { MacRomanFormatter(stringLength: 4, exactLengthRequired: true) } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementUSTR.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | // Implements USTR, UTXT, Unnn 5 | class ElementUSTR: ElementCSTR { 6 | override func configurePadding() { 7 | switch type { 8 | case "USTR": 9 | padding = .c 10 | case "UTXT": 11 | padding = .none 12 | unbounded = true 13 | default: 14 | let nnn = BaseElement.variableTypeValue(type) 15 | padding = .fixed(nnn) 16 | maxLength = nnn-1 17 | } 18 | } 19 | 20 | override func configure(view: NSView) { 21 | super.configure(view: view) 22 | if maxLength < UInt32.max { 23 | let textField = view.subviews.last as! NSTextField 24 | textField.placeholderString = "\(type) (\(maxLength) bytes)" 25 | } 26 | } 27 | 28 | override func readData(from reader: BinaryDataReader) throws { 29 | let end = reader.data[reader.position...].firstIndex(of: 0) ?? reader.data.endIndex 30 | let length = min(end - reader.position, maxLength) 31 | 32 | do { 33 | value = try reader.readString(length: length, encoding: .utf8) 34 | } catch BinaryDataReaderError.stringDecodeFailure { 35 | throw TemplateError.dataMismatch(self) 36 | } 37 | try reader.advance(padding.length(length)) 38 | } 39 | 40 | override func writeData(to writer: BinaryDataWriter) { 41 | if value.utf8.count > maxLength { 42 | let index = value.utf8.index(value.startIndex, offsetBy: maxLength) 43 | value = String(value.prefix(upTo: index)) 44 | } 45 | 46 | try? writer.writeString(value, encoding: .utf8) 47 | writer.advance(padding.length(value.utf8.count)) 48 | } 49 | 50 | override var formatter: Formatter { 51 | self.sharedFormatter { UTF8BytesFormatter(maxBytes: maxLength) } 52 | } 53 | } 54 | 55 | class UTF8BytesFormatter: Formatter { 56 | var maxBytes = 0 57 | 58 | convenience init(maxBytes: Int = 0) { 59 | self.init() 60 | self.maxBytes = maxBytes 61 | } 62 | 63 | override func string(for obj: Any?) -> String? { 64 | return obj as? String 65 | } 66 | 67 | override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 68 | for string: String, 69 | errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool { 70 | if string.utf8.count > maxBytes { 71 | error?.pointee = String(format: NSLocalizedString("The value must be no more than %d bytes.", comment: ""), maxBytes) as NSString 72 | return false 73 | } 74 | obj?.pointee = string as AnyObject 75 | return true 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Elements/ElementWCOL.swift: -------------------------------------------------------------------------------- 1 | import RFSupport 2 | 3 | // Implements WCOL, LCOL 4 | class ElementWCOL: ElementCOLR { 5 | private var bits = 0 6 | 7 | override func configure() throws { 8 | switch T.bitWidth { 9 | case 16: 10 | // 15-bit colour 11 | bits = 5 12 | case 32: 13 | // 24-bit colour 00RRGGBB 14 | bits = 8 15 | default: 16 | break 17 | } 18 | mask = (1 << bits) - 1 19 | } 20 | 21 | override func readData(from reader: BinaryDataReader) throws { 22 | let tmp = UInt(try reader.read() as T) 23 | r = UInt16(tmp >> (bits*2) & mask) 24 | g = UInt16(tmp >> bits & mask) 25 | b = UInt16(tmp & mask) 26 | } 27 | 28 | override func writeData(to writer: BinaryDataWriter) { 29 | var tmp: T = 0 30 | tmp |= T(r) << (bits*2) 31 | tmp |= T(g) << bits 32 | tmp |= T(b) 33 | writer.write(tmp) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ResForge/Template Editor/LinkingComboBox.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | // An extension of NSComboBox that allows displaying a link button inside the cell 4 | // This is used by RSID and CASR to open referenced resources 5 | class LinkingComboBox: NSComboBox { 6 | override class var cellClass: AnyClass? { 7 | get { LinkingComboBoxCell.self } 8 | set { } 9 | } 10 | 11 | private let linkButton: NSButton 12 | var linkIcon: NSImage.Name? { 13 | (delegate as? LinkingComboBoxDelegate)?.linkIcon 14 | } 15 | 16 | override init(frame frameRect: NSRect) { 17 | // To accommodate the touch bar add icon on macOS 10.14, we need to allow a height of 22 18 | let buttonFrame = NSRect(x: frameRect.size.width - 36, y: 1, width: 12, height: 22) 19 | linkButton = NSButton(frame: buttonFrame) 20 | super.init(frame: frameRect) 21 | linkButton.isBordered = false 22 | linkButton.bezelStyle = .inline 23 | linkButton.image = NSImage(named: NSImage.followLinkFreestandingTemplateName) 24 | if #unavailable(macOS 11) { 25 | linkButton.imageScaling = .scaleProportionallyDown 26 | } 27 | linkButton.target = self 28 | linkButton.action = #selector(followLink(_:)) 29 | self.addSubview(linkButton) 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override func draw(_ dirtyRect: NSRect) { 37 | if let linkIcon { 38 | linkButton.image = NSImage(named: linkIcon) 39 | } 40 | if linkButton.isHidden != (linkIcon == nil) { 41 | // Toggle the button visibility 42 | linkButton.isHidden = !linkButton.isHidden 43 | // If currently editing the field, the clip view frame will need updating 44 | if let clip = subviews.last as? NSClipView { 45 | var frame = clip.frame 46 | frame.size.width += linkButton.isHidden ? 16 : -16 47 | clip.frame = frame 48 | } 49 | } 50 | super.draw(dirtyRect) 51 | } 52 | 53 | @objc private func followLink(_ sender: Any) { 54 | // Ensure value is committed and link is still valid before following 55 | if self.currentEditor() == nil || (self.window?.makeFirstResponder(nil) != false && linkIcon != nil) { 56 | (delegate as? LinkingComboBoxDelegate)?.followLink(sender) 57 | } 58 | } 59 | } 60 | 61 | class LinkingComboBoxCell: NSComboBoxCell { 62 | open override func drawingRect(forBounds rect: NSRect) -> NSRect { 63 | // Ensure the text does not overlap the link button 64 | var dRect = super.drawingRect(forBounds: rect) 65 | if let control = controlView as? LinkingComboBox, control.linkIcon != nil { 66 | dRect.size.width -= 16 67 | } 68 | return dRect 69 | } 70 | } 71 | 72 | @objc protocol LinkingComboBoxDelegate: NSComboBoxDelegate { 73 | var linkIcon: NSImage.Name? { get } 74 | func followLink(_ sender: Any) 75 | } 76 | -------------------------------------------------------------------------------- /ResForge/Template Editor/NCB/NCBExpression.swift: -------------------------------------------------------------------------------- 1 | import RFSupport 2 | 3 | protocol NCBExpression { 4 | static var usage: String { get } 5 | static func parse(_ input: String) throws -> Self 6 | func description(manager: RFEditorManager) -> String 7 | } 8 | -------------------------------------------------------------------------------- /ResForge/Template Editor/ResourceMappings.rsrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Template Editor/ResourceMappings.rsrc -------------------------------------------------------------------------------- /ResForge/Template Editor/TabbableOutlineView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | // Table views don't support tabbing between rows so we need to handle the key view loop manually 4 | class TabbableOutlineView: NSOutlineView { 5 | @objc func selectPreviousKeyView(_ sender: Any) { 6 | let view = self.window!.firstResponder as! NSView 7 | if self.numberOfRows == 0 || (view.previousValidKeyView != nil && view.previousValidKeyView != self) { 8 | self.window?.selectPreviousKeyView(view) 9 | return 10 | } 11 | // Loop through all rows. Break if we come back to where we started without having found anything. 12 | var row = self.row(for: view) // -1 if not found 13 | if row == -1 { 14 | row = self.numberOfRows 15 | } 16 | var i = row-1 17 | while i != row { 18 | if i != -1, var view = self.view(atColumn: 1, row: i, makeIfNecessary: true) { 19 | if !view.canBecomeKeyView { 20 | view = view.subviews.last { $0.canBecomeKeyView } ?? self.view(atColumn: 0, row: i, makeIfNecessary: true) ?? view 21 | } 22 | if view.canBecomeKeyView { 23 | self.window?.makeFirstResponder(view) 24 | view.scrollToVisible(view.superview!.bounds) 25 | return 26 | } 27 | } 28 | i = i == -1 ? self.numberOfRows-1 : i-1 29 | } 30 | } 31 | 32 | @objc func selectNextKeyView(_ sender: Any) { 33 | let view = self.window!.firstResponder as! NSView 34 | if self.numberOfRows == 0 || view.nextValidKeyView != nil { 35 | self.window?.selectNextKeyView(view) 36 | return 37 | } 38 | var row = self.row(for: view) 39 | var i = row+1 40 | if row == -1 { 41 | row = self.numberOfRows 42 | } 43 | while i != row { 44 | if i != self.numberOfRows, var view = self.view(atColumn: 0, row: i, makeIfNecessary: true) { 45 | if !view.canBecomeKeyView, let cell = self.view(atColumn: 1, row: i, makeIfNecessary: true) { 46 | view = cell.subviews.first { $0.canBecomeKeyView } ?? cell 47 | } 48 | if view.canBecomeKeyView { 49 | self.window?.makeFirstResponder(view) 50 | view.scrollToVisible(view.superview!.bounds) 51 | return 52 | } 53 | } 54 | i = i == self.numberOfRows ? 0 : i+1 55 | } 56 | } 57 | 58 | // Don't draw the disclosure triangles 59 | override func frameOfOutlineCell(atRow row: Int) -> NSRect { 60 | return .zero 61 | } 62 | 63 | // Manually manage indentation for list headers 64 | override func frameOfCell(atColumn column: Int, row: Int) -> NSRect { 65 | var superFrame = super.frameOfCell(atColumn: column, row: row) 66 | if column == 0 && self.item(atRow: row) is ElementLSTB { 67 | let indent = self.level(forRow: row) * 16 68 | superFrame.origin.x += CGFloat(indent) 69 | superFrame.size.width -= CGFloat(indent) 70 | } 71 | return superFrame 72 | } 73 | 74 | // Add bottom padding 75 | override func setFrameSize(_ newSize: NSSize) { 76 | super.setFrameSize(NSSize(width: newSize.width, height: newSize.height+5)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ResForge/Template Editor/TemplateDataView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class TemplateDataView: NSView { 4 | // Data cells use flipped coordinates, drawing from top to bottom 5 | override var isFlipped: Bool { 6 | return true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ResForge/Template Editor/TemplateError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RFSupport 3 | 4 | enum TemplateError: LocalizedError, RecoverableError { 5 | case corrupt 6 | case unknownElement(String) 7 | case unclosedElement(CollectionElement) 8 | case unboundedElement(BaseElement) 9 | case invalidStructure(BaseElement, String) 10 | case dataMismatch(BaseElement) 11 | case truncate 12 | 13 | var errorDescription: String? { 14 | switch self { 15 | case .corrupt: 16 | return NSLocalizedString("Corrupt or insufficient data.", comment: "") 17 | case let .unknownElement(type): 18 | return String(format: NSLocalizedString("Unsupported element type ‘%@’.", comment: ""), type) 19 | case let .unclosedElement(element): 20 | return "\(element.type) “\(element.label)”: ".appendingFormat(NSLocalizedString("Closing ‘%@’ element not found.", comment: ""), element.endType) 21 | case let .unboundedElement(element as CollectionElement): 22 | let message = String(format: NSLocalizedString("Closing ‘%@’ must be last element in template or sized section.", comment: ""), element.endType) 23 | return "\(element.type) “\(element.label)”: ".appending(message) 24 | case let .unboundedElement(element): 25 | let message = NSLocalizedString("Must be last element in template or sized section.", comment: "") 26 | return "\(element.type) “\(element.label)”: ".appending(message) 27 | case let .invalidStructure(element, message): 28 | return "\(element.type) “\(element.label)”: ".appending(message) 29 | case .dataMismatch: 30 | return NSLocalizedString("The resource’s data cannot be interpreted by the template.", comment: "") 31 | case .truncate: 32 | return NSLocalizedString("The resource contains more data than will fit the template.", comment: "") 33 | } 34 | } 35 | 36 | var recoverySuggestion: String? { 37 | switch self { 38 | case .truncate: 39 | return NSLocalizedString("Saving the resource will truncate the data.", comment: "") 40 | default: 41 | return nil 42 | } 43 | } 44 | 45 | var recoveryOptions: [String] { 46 | switch self { 47 | case .dataMismatch: 48 | return [NSLocalizedString("OK", comment: ""), NSLocalizedString("Open with Hex Editor", comment: "")] 49 | default: 50 | return [] 51 | } 52 | } 53 | 54 | func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool { 55 | if recoveryOptionIndex == 1, case let .dataMismatch(element) = self { 56 | let controller = element.parentList.controller! 57 | controller.manager.open(resource: controller.resource, using: PluginRegistry.hexEditor) 58 | } 59 | return false 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ResForge/Template Editor/TemplateLabelView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import RFSupport 3 | 4 | public extension NSPasteboard.PasteboardType { 5 | static let RFTemplateListItem = Self("com.resforge.template-list-item") 6 | } 7 | 8 | /// Focusable label view for list headers, allows creating and deleting entries. 9 | class TemplateLabelView: NSTableCellView { 10 | override var acceptsFirstResponder: Bool { 11 | return true 12 | } 13 | 14 | private var dataList: NSOutlineView? { 15 | superview?.superview as? NSOutlineView 16 | } 17 | 18 | override func mouseDown(with event: NSEvent) { 19 | self.window?.makeFirstResponder(self) 20 | } 21 | 22 | override var focusRingMaskBounds: NSRect { 23 | return self.textField!.frame 24 | } 25 | 26 | override func drawFocusRingMask() { 27 | self.focusRingMaskBounds.fill() 28 | } 29 | 30 | @IBAction func cut(_ sender: Any) { 31 | self.copy(sender) 32 | self.delete(sender) 33 | } 34 | 35 | @IBAction func copy(_ sender: Any) { 36 | guard let dataList else { return } 37 | let row = dataList.row(for: self) 38 | if let element = dataList.item(atRow: row) as? ElementLSTB { 39 | let writer = BinaryDataWriter() 40 | element.writeData(to: writer) 41 | NSPasteboard.general.declareTypes([.RFTemplateListItem], owner: nil) 42 | NSPasteboard.general.setData(writer.data, forType: .RFTemplateListItem) 43 | } 44 | } 45 | 46 | @IBAction func paste(_ sender: Any) { 47 | if let data = NSPasteboard.general.data(forType: .RFTemplateListItem) { 48 | self.createListItem(data) 49 | } 50 | } 51 | 52 | @IBAction func createNewItem(_ sender: Any) { 53 | self.createListItem() 54 | } 55 | 56 | private func createListItem(_ data: Data? = nil) { 57 | guard let dataList else { return } 58 | let row = dataList.row(for: self) 59 | if let element = dataList.item(atRow: row) as? ElementLSTB, element.createListEntry(data) { 60 | dataList.reloadData() 61 | let newHeader = dataList.view(atColumn: 0, row: row, makeIfNecessary: true) 62 | window?.makeFirstResponder(newHeader) 63 | // Expand the item and scroll the new content into view 64 | dataList.expandItem(dataList.item(atRow: row), expandChildren: true) 65 | let lastChild = dataList.rowView(atRow: dataList.row(forItem: element), makeIfNecessary: true) 66 | lastChild?.scrollToVisible(lastChild!.bounds) 67 | newHeader?.scrollToVisible(newHeader!.superview!.bounds) 68 | } 69 | } 70 | 71 | @IBAction func delete(_ sender: Any) { 72 | guard let dataList else { return } 73 | let row = dataList.row(for: self) 74 | if let element = dataList.item(atRow: row) as? ElementLSTB, element.removeListEntry() { 75 | dataList.reloadData() 76 | let newHeader = dataList.view(atColumn: 0, row: row, makeIfNecessary: true) 77 | window?.makeFirstResponder(newHeader) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ResForge/Template Editor/Templates.rsrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrews05/ResForge/2abe1df63bf7daa95fd3373fd951425c73639547/ResForge/Template Editor/Templates.rsrc --------------------------------------------------------------------------------