├── .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 | 
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
--------------------------------------------------------------------------------