Adds ability for user to add/edit custom cheats
This commit is contained in:
parent
f0f88a529a
commit
f7ed6f75d1
@ -1 +1 @@
|
||||
Subproject commit 9e14654abbcf82ff415eb6fb2648b0be5091ca44
|
||||
Subproject commit a20b36f96e52bc236d243cd8b1b55fc5e1bbf76c
|
||||
@ -1 +1 @@
|
||||
Subproject commit 87d9cfa5574fe42db453f9b6b08b3e7190a4b7d2
|
||||
Subproject commit cac97f68eea3eb85471a16b468ae8da6fa1038e6
|
||||
@ -25,6 +25,8 @@
|
||||
BF2A53FD1BB74FC60052BD0C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; };
|
||||
BF2A53FE1BB74FC60052BD0C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; };
|
||||
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */; };
|
||||
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA101CF1899D006624C7 /* CheatTextView.swift */; };
|
||||
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; };
|
||||
BF353FF31C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; };
|
||||
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF353FF41C5D837600C1184C /* PauseMenu.storyboard */; };
|
||||
@ -139,6 +141,8 @@
|
||||
BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
|
||||
BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesCollectionHeaderView.swift; path = "Pause Menu/Save States/SaveStatesCollectionHeaderView.swift"; sourceTree = "<group>"; };
|
||||
BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditCheatViewController.swift; path = "Pause Menu/Cheats/EditCheatViewController.swift"; sourceTree = "<group>"; };
|
||||
BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatTextView.swift; path = "Pause Menu/Cheats/CheatTextView.swift"; sourceTree = "<group>"; };
|
||||
BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = "<group>"; };
|
||||
BF353FF51C5D837600C1184C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PauseMenu.storyboard; sourceTree = "<group>"; };
|
||||
BF353FF81C5D870B00C1184C /* PauseItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseItem.swift; path = "Pause Menu/PauseItem.swift"; sourceTree = "<group>"; };
|
||||
@ -388,6 +392,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */,
|
||||
BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */,
|
||||
BF34FA101CF1899D006624C7 /* CheatTextView.swift */,
|
||||
);
|
||||
name = Cheats;
|
||||
sourceTree = "<group>";
|
||||
@ -730,8 +736,10 @@
|
||||
BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */,
|
||||
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,
|
||||
BF7AE81E1C2E984300B1B5BC /* GridCollectionViewCell.swift in Sources */,
|
||||
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
|
||||
BF4566E81BC090B6007BFA1A /* Model.xcdatamodeld in Sources */,
|
||||
BFDE393C1BC0CEDF003F72E8 /* Game.swift in Sources */,
|
||||
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
|
||||
BFC2731A1BE6152200D22B05 /* GameCollection.swift in Sources */,
|
||||
BFF1E5641BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */,
|
||||
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15E65" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="Dt0-nV-isV">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="Dt0-nV-isV">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Pause View Controller-->
|
||||
@ -226,5 +227,143 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1702" y="1377"/>
|
||||
</scene>
|
||||
<!--Cheat-->
|
||||
<scene sceneID="SMZ-Ah-CVb">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="editCheatViewController" id="jTR-Oe-YUJ" customClass="EditCheatViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Name" id="QT6-DZ-g70">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ZeC-rg-QFa">
|
||||
<rect key="frame" x="0.0" y="113.5" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Cheat Name" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="DD1-X0-hg7">
|
||||
<rect key="frame" x="15" y="0.0" width="570" height="43.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/>
|
||||
<connections>
|
||||
<action selector="textFieldDidEndEditing:" destination="jTR-Oe-YUJ" eventType="editingDidEnd" id="RcS-1I-sHp"/>
|
||||
<action selector="updateCheatName:" destination="jTR-Oe-YUJ" eventType="editingChanged" id="ZMg-n2-rsf"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="DD1-X0-hg7" firstAttribute="top" secondItem="UIF-fK-ApW" secondAttribute="top" id="3Ta-uE-hx1"/>
|
||||
<constraint firstItem="DD1-X0-hg7" firstAttribute="trailing" secondItem="UIF-fK-ApW" secondAttribute="trailingMargin" id="Igk-iU-oK0"/>
|
||||
<constraint firstItem="DD1-X0-hg7" firstAttribute="leading" secondItem="UIF-fK-ApW" secondAttribute="leadingMargin" id="ZPW-fc-ZhS"/>
|
||||
<constraint firstAttribute="bottom" secondItem="DD1-X0-hg7" secondAttribute="bottom" id="n6Z-kr-dcR"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Type" footerTitle="Description" id="rvn-VK-2uH">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Tst-zn-e04">
|
||||
<rect key="frame" x="0.0" y="207.5" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q">
|
||||
<rect key="frame" x="15" y="7" width="570" height="29"/>
|
||||
<segments>
|
||||
<segment title="First"/>
|
||||
<segment title="Second"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="updateCheatType:" destination="jTR-Oe-YUJ" eventType="valueChanged" id="ex4-x7-yaK"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="xrD-ue-96Q" firstAttribute="centerY" secondItem="gwV-zS-RWQ" secondAttribute="centerY" id="HEE-6o-6oQ"/>
|
||||
<constraint firstItem="xrD-ue-96Q" firstAttribute="leading" secondItem="gwV-zS-RWQ" secondAttribute="leadingMargin" id="e07-ea-lKZ"/>
|
||||
<constraint firstItem="xrD-ue-96Q" firstAttribute="trailing" secondItem="gwV-zS-RWQ" secondAttribute="trailingMargin" id="qk0-rE-QTg"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Code" footerTitle="Description" id="rHC-nA-ga0">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="210" id="xxc-cz-sb7">
|
||||
<rect key="frame" x="0.0" y="318" width="600" height="210"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="209.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="209.5"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="jTR-Oe-YUJ" id="n4b-FA-7Dd"/>
|
||||
</connections>
|
||||
</textView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="a17-LB-QXD" secondAttribute="bottom" id="3XH-aF-bNE"/>
|
||||
<constraint firstItem="a17-LB-QXD" firstAttribute="top" secondItem="agU-SE-fNa" secondAttribute="topMargin" id="NxO-eC-sk6"/>
|
||||
<constraint firstItem="a17-LB-QXD" firstAttribute="leading" secondItem="agU-SE-fNa" secondAttribute="leadingMargin" id="QwM-HU-6cp"/>
|
||||
<constraint firstAttribute="trailing" secondItem="a17-LB-QXD" secondAttribute="trailing" id="WpC-Va-wtg"/>
|
||||
<constraint firstItem="a17-LB-QXD" firstAttribute="leading" secondItem="agU-SE-fNa" secondAttribute="leading" id="acf-fI-y13"/>
|
||||
<constraint firstItem="a17-LB-QXD" firstAttribute="top" secondItem="agU-SE-fNa" secondAttribute="top" id="gcg-A8-DX9"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="a17-LB-QXD" secondAttribute="bottom" id="hex-Ie-1rn"/>
|
||||
<constraint firstItem="a17-LB-QXD" firstAttribute="trailing" secondItem="agU-SE-fNa" secondAttribute="trailingMargin" id="ilf-Gt-J7Y"/>
|
||||
</constraints>
|
||||
<variation key="default">
|
||||
<mask key="constraints">
|
||||
<exclude reference="NxO-eC-sk6"/>
|
||||
<exclude reference="QwM-HU-6cp"/>
|
||||
<exclude reference="hex-Ie-1rn"/>
|
||||
<exclude reference="ilf-Gt-J7Y"/>
|
||||
</mask>
|
||||
</variation>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="jTR-Oe-YUJ" id="IdU-96-UyX"/>
|
||||
<outlet property="delegate" destination="jTR-Oe-YUJ" id="7X0-hO-jy3"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Cheat" id="zdc-ES-dan">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="MFY-SZ-J7p">
|
||||
<connections>
|
||||
<segue destination="d4f-Cp-p2b" kind="unwind" identifier="unwindEditCheatSegue" unwindAction="unwindFromEditCheatViewController:" id="rC5-FR-JoB"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="save" id="C7k-Kp-CF3">
|
||||
<connections>
|
||||
<action selector="saveCheat:" destination="jTR-Oe-YUJ" id="Goi-Ob-IUg"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="codeTextView" destination="a17-LB-QXD" id="dgZ-U8-doW"/>
|
||||
<outlet property="nameTextField" destination="DD1-X0-hg7" id="y36-wg-Min"/>
|
||||
<outlet property="typeSegmentedControl" destination="xrD-ue-96Q" id="wNy-cq-NM2"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x1S-Un-UKy" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<exit id="d4f-Cp-p2b" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2385" y="1377"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@ -342,9 +342,9 @@ extension EmulationViewController: SaveStatesViewControllerDelegate
|
||||
/// Cheats
|
||||
extension EmulationViewController: CheatsViewControllerDelegate
|
||||
{
|
||||
func cheatsViewControllerActiveGame(cheatsViewController: CheatsViewController) -> Game
|
||||
func cheatsViewControllerActiveEmulatorCore(saveStatesViewController: CheatsViewController) -> EmulatorCore
|
||||
{
|
||||
return self.emulatorCore.game as! Game
|
||||
return self.emulatorCore
|
||||
}
|
||||
|
||||
func cheatsViewController(cheatsViewController: CheatsViewController, didActivateCheat cheat: Cheat) throws
|
||||
|
||||
166
Delta/Pause Menu/Cheats/CheatTextView.swift
Normal file
166
Delta/Pause Menu/Cheats/CheatTextView.swift
Normal file
@ -0,0 +1,166 @@
|
||||
//
|
||||
// CheatTextView.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 5/22/16.
|
||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreText
|
||||
|
||||
import DeltaCore
|
||||
|
||||
import Roxas
|
||||
|
||||
private let CheatPrefixAttribute = "prefix"
|
||||
|
||||
class CheatTextView: UITextView
|
||||
{
|
||||
var cheatFormat: CheatFormat? {
|
||||
didSet {
|
||||
self.updateAttributedFormat()
|
||||
}
|
||||
}
|
||||
|
||||
@NSCopying private var attributedFormat: NSAttributedString?
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.layoutManager.delegate = self
|
||||
|
||||
self.textContainer.widthTracksTextView = false
|
||||
self.textContainer.heightTracksTextView = false
|
||||
}
|
||||
}
|
||||
|
||||
extension CheatTextView
|
||||
{
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
if let format = self.cheatFormat, font = self.font
|
||||
{
|
||||
let characterWidth = ("A" as NSString).sizeWithAttributes([NSFontAttributeName: font]).width
|
||||
|
||||
let width = characterWidth * CGFloat(format.format.characters.count)
|
||||
self.textContainer.size = CGSize(width: width, height: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CheatTextView
|
||||
{
|
||||
func updateAttributedFormat()
|
||||
{
|
||||
guard let format = self.cheatFormat?.format else
|
||||
{
|
||||
self.attributedFormat = nil
|
||||
return
|
||||
}
|
||||
|
||||
let attributedFormat = NSMutableAttributedString()
|
||||
var prefixString: NSString? = nil
|
||||
|
||||
let scanner = NSScanner(string: format)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
while (!scanner.atEnd)
|
||||
{
|
||||
var string: NSString? = nil
|
||||
scanner.scanCharactersFromSet(NSCharacterSet.alphanumericCharacterSet(), intoString: &string)
|
||||
|
||||
guard let scannedString = string where scannedString.length > 0 else { break }
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: scannedString as String)
|
||||
|
||||
if let prefixString = prefixString where prefixString.length > 0
|
||||
{
|
||||
attributedString.addAttribute(CheatPrefixAttribute, value: prefixString, range: NSRange(location: 0, length: 1))
|
||||
}
|
||||
|
||||
attributedFormat.appendAttributedString(attributedString)
|
||||
|
||||
prefixString = nil
|
||||
scanner.scanUpToCharactersFromSet(NSCharacterSet.alphanumericCharacterSet(), intoString: &prefixString)
|
||||
}
|
||||
|
||||
self.attributedFormat = attributedFormat
|
||||
|
||||
rst_dispatch_sync_on_main_thread {
|
||||
self.setNeedsLayout()
|
||||
self.layoutIfNeeded()
|
||||
|
||||
let range = NSRange(location: 0, length: (self.text as NSString).length)
|
||||
self.layoutManager.invalidateGlyphsForCharacterRange(range, changeInLength: 0, actualCharacterRange: nil)
|
||||
self.layoutManager.invalidateLayoutForCharacterRange(range, actualCharacterRange: nil)
|
||||
self.layoutManager.ensureGlyphsForCharacterRange(range)
|
||||
self.layoutManager.ensureLayoutForCharacterRange(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CheatTextView: NSLayoutManagerDelegate
|
||||
{
|
||||
func layoutManager(layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSGlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: UIFont, forGlyphRange glyphRange: NSRange) -> Int
|
||||
{
|
||||
// Returning 0 = let the layoutManager do the normal logic
|
||||
guard let attributedFormat = self.attributedFormat else { return 0 }
|
||||
|
||||
let glyphCount = glyphRange.length
|
||||
|
||||
// Ensure the buffer is long enough to hold our additional glyphs
|
||||
// If we're only modifying one character, glyphCount * 2 = 2, which is not large enough if we're inserting multiple separator characters
|
||||
let bufferSize = max(attributedFormat.length + 1, glyphCount * 2)
|
||||
|
||||
// Allocate our replacement buffers
|
||||
let glyphBuffer = UnsafeMutablePointer<CGGlyph>.alloc(bufferSize)
|
||||
let propertyBuffer = UnsafeMutablePointer<NSGlyphProperty>.alloc(bufferSize)
|
||||
let characterBuffer = UnsafeMutablePointer<Int>.alloc(bufferSize)
|
||||
|
||||
var offset = 0
|
||||
|
||||
for i in 0 ..< glyphCount
|
||||
{
|
||||
// The index the actual character maps to in the cheat format
|
||||
let characterIndex = charIndexes[i] % attributedFormat.length
|
||||
|
||||
if let prefix = attributedFormat.attributesAtIndex(characterIndex, effectiveRange: nil)[CheatPrefixAttribute] as? String
|
||||
{
|
||||
// If there is a prefix string, we insert the glyphs (and associated properties/character indexes) first
|
||||
let prefixCount = prefix.characters.count
|
||||
|
||||
for j in 0 ..< prefixCount
|
||||
{
|
||||
characterBuffer[i + offset + j] = charIndexes[i]
|
||||
propertyBuffer[i + offset + j] = props[i]
|
||||
|
||||
// Prepend prefix character
|
||||
var prefixCharacter = (prefix as NSString).characterAtIndex(0)
|
||||
CTFontGetGlyphsForCharacters(aFont as CTFont, &prefixCharacter, glyphBuffer + (i + offset + j), 1)
|
||||
}
|
||||
|
||||
offset += prefixCount
|
||||
}
|
||||
|
||||
// Copy over the information from the original buffers
|
||||
characterBuffer[i + offset] = charIndexes[i]
|
||||
propertyBuffer[i + offset] = props[i]
|
||||
glyphBuffer[i + offset] = glyphs[i]
|
||||
}
|
||||
|
||||
// Replace buffers with our own buffers, and ensure length takes into account any added glpyhs
|
||||
layoutManager.setGlyphs(glyphBuffer, properties: propertyBuffer, characterIndexes: characterBuffer, font: aFont, forGlyphRange: NSRange(location: glyphRange.location, length: glyphCount + offset))
|
||||
|
||||
// Clean up memory
|
||||
characterBuffer.dealloc(bufferSize)
|
||||
propertyBuffer.dealloc(bufferSize)
|
||||
glyphBuffer.dealloc(bufferSize)
|
||||
|
||||
// Return total number of glyphs
|
||||
return glyphCount + offset
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@ import Roxas
|
||||
|
||||
protocol CheatsViewControllerDelegate: class
|
||||
{
|
||||
func cheatsViewControllerActiveGame(saveStatesViewController: CheatsViewController) -> Game
|
||||
func cheatsViewControllerActiveEmulatorCore(saveStatesViewController: CheatsViewController) -> EmulatorCore
|
||||
func cheatsViewController(cheatsViewController: CheatsViewController, didActivateCheat cheat: Cheat) throws
|
||||
func cheatsViewController(cheatsViewController: CheatsViewController, didDeactivateCheat cheat: Cheat) throws
|
||||
}
|
||||
@ -67,13 +67,21 @@ extension CheatsViewController
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Navigation -
|
||||
private extension CheatsViewController
|
||||
{
|
||||
@IBAction func unwindFromEditCheatViewController(segue: UIStoryboardSegue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Update -
|
||||
private extension CheatsViewController
|
||||
{
|
||||
//MARK: - Update -
|
||||
|
||||
func updateFetchedResultsController()
|
||||
{
|
||||
let game = self.delegate.cheatsViewControllerActiveGame(self)
|
||||
let game = self.delegate.cheatsViewControllerActiveEmulatorCore(self).game as! Game
|
||||
|
||||
let fetchRequest = Cheat.fetchRequest()
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
@ -105,42 +113,8 @@ private extension CheatsViewController
|
||||
{
|
||||
@IBAction func addCheat()
|
||||
{
|
||||
let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext()
|
||||
backgroundContext.performBlock {
|
||||
|
||||
var game = self.delegate.cheatsViewControllerActiveGame(self)
|
||||
game = backgroundContext.objectWithID(game.objectID) as! Game
|
||||
|
||||
let cheat = Cheat.insertIntoManagedObjectContext(backgroundContext)
|
||||
cheat.game = game
|
||||
cheat.name = "Unlimited Jumps"
|
||||
cheat.code = "3E2C-AF6F"
|
||||
cheat.type = .gameGenie
|
||||
|
||||
do
|
||||
{
|
||||
try self.delegate.cheatsViewController(self, didActivateCheat: cheat)
|
||||
backgroundContext.saveWithErrorLogging()
|
||||
}
|
||||
catch EmulatorCore.CheatError.invalid
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Invalid Cheat", comment: ""), message: NSLocalizedString("Please make sure you typed the cheat code in correctly and try again.", comment: ""), preferredStyle: .Alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .Default, handler: nil))
|
||||
self.presentViewController(alertController, animated: true, completion: nil)
|
||||
|
||||
}
|
||||
|
||||
print("Invalid cheat:", cheat.name, cheat.code)
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
print("Unknown Cheat Error:", error, cheat.name, cheat.code)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
let editCheatViewController = self.makeEditCheatViewController(cheat: nil)
|
||||
self.presentViewController(RSTContainInNavigationController(editCheatViewController), animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func deleteCheat(cheat: Cheat)
|
||||
@ -156,8 +130,8 @@ private extension CheatsViewController
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Content -
|
||||
/// Content
|
||||
//MARK: - Convenience -
|
||||
/// Convenience
|
||||
private extension CheatsViewController
|
||||
{
|
||||
func configure(cell cell: UITableViewCell, forIndexPath indexPath: NSIndexPath)
|
||||
@ -167,6 +141,17 @@ private extension CheatsViewController
|
||||
cell.textLabel?.font = UIFont.boldSystemFontOfSize(cell.textLabel!.font.pointSize)
|
||||
cell.accessoryType = cheat.enabled ? .Checkmark : .None
|
||||
}
|
||||
|
||||
func makeEditCheatViewController(cheat cheat: Cheat?) -> EditCheatViewController
|
||||
{
|
||||
let editCheatViewController = self.storyboard!.instantiateViewControllerWithIdentifier("editCheatViewController") as! EditCheatViewController
|
||||
editCheatViewController.delegate = self
|
||||
editCheatViewController.supportedCheatFormats = self.delegate.cheatsViewControllerActiveEmulatorCore(self).supportedCheatFormats
|
||||
editCheatViewController.cheat = cheat
|
||||
editCheatViewController.game = self.delegate.cheatsViewControllerActiveEmulatorCore(self).game as! Game
|
||||
|
||||
return editCheatViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension CheatsViewController
|
||||
@ -236,13 +221,15 @@ extension CheatsViewController
|
||||
|
||||
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]?
|
||||
{
|
||||
let cheat = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Cheat
|
||||
|
||||
let deleteAction = UITableViewRowAction(style: .Destructive, title: NSLocalizedString("Delete", comment: "")) { (action, indexPath) in
|
||||
let cheat = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Cheat
|
||||
self.deleteCheat(cheat)
|
||||
}
|
||||
|
||||
let editAction = UITableViewRowAction(style: .Normal, title: NSLocalizedString("Edit", comment: "")) { (action, indexPath) in
|
||||
|
||||
let editCheatViewController = self.makeEditCheatViewController(cheat: cheat)
|
||||
self.presentViewController(RSTContainInNavigationController(editCheatViewController), animated: true, completion: nil)
|
||||
}
|
||||
|
||||
return [deleteAction, editAction]
|
||||
@ -254,6 +241,34 @@ extension CheatsViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension CheatsViewController: EditCheatViewControllerDelegate
|
||||
{
|
||||
func editCheatViewController(editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) throws
|
||||
{
|
||||
try self.delegate.cheatsViewController(self, didActivateCheat: cheat)
|
||||
|
||||
if let previousCheat = previousCheat
|
||||
{
|
||||
let code = cheat.code
|
||||
|
||||
previousCheat.managedObjectContext?.performBlockAndWait({
|
||||
|
||||
guard previousCheat.code != code else { return }
|
||||
|
||||
do
|
||||
{
|
||||
try self.delegate.cheatsViewController(self, didDeactivateCheat: previousCheat)
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - <NSFetchedResultsControllerDelegate> -
|
||||
extension CheatsViewController: NSFetchedResultsControllerDelegate
|
||||
{
|
||||
|
||||
329
Delta/Pause Menu/Cheats/EditCheatViewController.swift
Normal file
329
Delta/Pause Menu/Cheats/EditCheatViewController.swift
Normal file
@ -0,0 +1,329 @@
|
||||
//
|
||||
// EditCheatViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 5/21/16.
|
||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
import DeltaCore
|
||||
import Roxas
|
||||
|
||||
protocol EditCheatViewControllerDelegate: class
|
||||
{
|
||||
func editCheatViewController(editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) throws
|
||||
}
|
||||
|
||||
private extension EditCheatViewController
|
||||
{
|
||||
enum ValidationError: ErrorType
|
||||
{
|
||||
case invalidCode
|
||||
case duplicateName
|
||||
case duplicateCode
|
||||
}
|
||||
|
||||
enum Section: Int
|
||||
{
|
||||
case name
|
||||
case type
|
||||
case code
|
||||
}
|
||||
}
|
||||
|
||||
class EditCheatViewController: UITableViewController
|
||||
{
|
||||
weak var delegate: EditCheatViewControllerDelegate?
|
||||
|
||||
var cheat: Cheat?
|
||||
var game: Game!
|
||||
var supportedCheatFormats: [CheatFormat]!
|
||||
|
||||
private var selectedCheatFormat: CheatFormat {
|
||||
let cheatFormat = self.supportedCheatFormats[self.typeSegmentedControl.selectedSegmentIndex]
|
||||
return cheatFormat
|
||||
}
|
||||
|
||||
private var mutableCheat: Cheat!
|
||||
private var managedObjectContext = DatabaseManager.sharedManager.backgroundManagedObjectContext()
|
||||
|
||||
@IBOutlet private var nameTextField: UITextField!
|
||||
@IBOutlet private var typeSegmentedControl: UISegmentedControl!
|
||||
@IBOutlet private var codeTextView: CheatTextView!
|
||||
}
|
||||
|
||||
extension EditCheatViewController
|
||||
{
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
var name: String!
|
||||
var type: CheatType!
|
||||
var code: String!
|
||||
|
||||
self.managedObjectContext.performBlockAndWait {
|
||||
|
||||
// Main Thread context is read-only, so we either create a new cheat, or get a reference to the current cheat in a new background context
|
||||
|
||||
if let cheat = self.cheat
|
||||
{
|
||||
self.mutableCheat = self.managedObjectContext.objectWithID(cheat.objectID) as? Cheat
|
||||
}
|
||||
else
|
||||
{
|
||||
self.mutableCheat = Cheat.insertIntoManagedObjectContext(self.managedObjectContext)
|
||||
self.mutableCheat.game = self.managedObjectContext.objectWithID(self.game.objectID) as! Game
|
||||
self.mutableCheat.type = self.supportedCheatFormats.first!.type
|
||||
self.mutableCheat.code = ""
|
||||
self.mutableCheat.name = ""
|
||||
}
|
||||
|
||||
self.mutableCheat.enabled = true // After we save a cheat, it should be enabled
|
||||
|
||||
name = self.mutableCheat.name
|
||||
type = self.mutableCheat.type
|
||||
code = self.mutableCheat.code.sanitized(characterSet: self.selectedCheatFormat.allowedCodeCharacters)
|
||||
}
|
||||
|
||||
|
||||
// Update UI
|
||||
|
||||
if name.characters.count == 0
|
||||
{
|
||||
self.title = NSLocalizedString("Cheat", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
self.title = name
|
||||
}
|
||||
|
||||
self.nameTextField.text = name
|
||||
self.codeTextView.text = code
|
||||
|
||||
self.typeSegmentedControl.removeAllSegments()
|
||||
|
||||
for (index, format) in self.supportedCheatFormats.enumerate()
|
||||
{
|
||||
self.typeSegmentedControl.insertSegmentWithTitle(format.name, atIndex: index, animated: false)
|
||||
}
|
||||
|
||||
if let index = self.supportedCheatFormats.indexOf({ $0.type == type })
|
||||
{
|
||||
self.typeSegmentedControl.selectedSegmentIndex = index
|
||||
}
|
||||
else
|
||||
{
|
||||
self.typeSegmentedControl.selectedSegmentIndex = 0
|
||||
}
|
||||
|
||||
self.updateCheatType(self.typeSegmentedControl)
|
||||
self.updateSaveButtonState()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
if let superview = self.codeTextView.superview
|
||||
{
|
||||
let layoutMargins = superview.layoutMargins
|
||||
self.codeTextView.textContainerInset = layoutMargins
|
||||
|
||||
self.codeTextView.textContainer.lineFragmentPadding = 0
|
||||
}
|
||||
|
||||
if self.appearing
|
||||
{
|
||||
self.nameTextField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning()
|
||||
{
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
}
|
||||
|
||||
private extension EditCheatViewController
|
||||
{
|
||||
@IBAction func updateCheatName(sender: UITextField)
|
||||
{
|
||||
var title = sender.text ?? ""
|
||||
if title.characters.count == 0
|
||||
{
|
||||
title = NSLocalizedString("Cheat", comment: "")
|
||||
}
|
||||
|
||||
self.title = title
|
||||
|
||||
self.updateSaveButtonState()
|
||||
}
|
||||
|
||||
@IBAction func updateCheatType(sender: UISegmentedControl)
|
||||
{
|
||||
self.codeTextView.cheatFormat = self.selectedCheatFormat
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadSections(NSIndexSet(index: Section.type.rawValue), withRowAnimation: .None)
|
||||
|
||||
// Hacky-ish workaround so we can update the footer text for the code section without causing text view to resign first responder status
|
||||
self.tableView.beginUpdates()
|
||||
|
||||
if let footerView = self.tableView.footerViewForSection(Section.code.rawValue)
|
||||
{
|
||||
footerView.textLabel!.text = self.tableView(self.tableView, titleForFooterInSection: Section.code.rawValue)
|
||||
footerView.sizeToFit()
|
||||
}
|
||||
|
||||
self.tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSaveButtonState()
|
||||
{
|
||||
let isValidName = !(self.nameTextField.text ?? "").isEmpty
|
||||
let isValidCode = !self.codeTextView.text.isEmpty
|
||||
|
||||
self.navigationItem.rightBarButtonItem?.enabled = isValidName && isValidCode
|
||||
}
|
||||
|
||||
@IBAction func saveCheat(sender: UIBarButtonItem)
|
||||
{
|
||||
self.mutableCheat.managedObjectContext?.performBlockAndWait {
|
||||
|
||||
self.mutableCheat.name = self.nameTextField.text ?? ""
|
||||
self.mutableCheat.type = self.selectedCheatFormat.type
|
||||
self.mutableCheat.code = self.codeTextView.text.formatted(cheatFormat: self.selectedCheatFormat)
|
||||
|
||||
do
|
||||
{
|
||||
try self.validateCheat(self.mutableCheat)
|
||||
self.mutableCheat.managedObjectContext?.saveWithErrorLogging()
|
||||
self.performSegueWithIdentifier("unwindEditCheatSegue", sender: sender)
|
||||
}
|
||||
catch ValidationError.invalidCode
|
||||
{
|
||||
self.presentErrorAlert(title: NSLocalizedString("Invalid Code", comment: ""), message: NSLocalizedString("Please make sure you typed the cheat code in correctly and try again.", comment: "")) {
|
||||
self.codeTextView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
catch ValidationError.duplicateCode
|
||||
{
|
||||
self.presentErrorAlert(title: NSLocalizedString("Duplicate Code", comment: ""), message: NSLocalizedString("A cheat already exists with this code. Please type in a different code and try again.", comment: "")) {
|
||||
self.codeTextView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
catch ValidationError.duplicateName
|
||||
{
|
||||
self.presentErrorAlert(title: NSLocalizedString("Duplicate Name", comment: ""), message: NSLocalizedString("A cheat already exists with this name. Please rename this cheat and try again.", comment: "")) {
|
||||
self.nameTextField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateCheat(cheat: Cheat) throws
|
||||
{
|
||||
let name = cheat.name!
|
||||
let code = cheat.code
|
||||
|
||||
// Find all cheats that are for the same game, don't have the same identifier as the current cheat, but have either the same name or code
|
||||
let predicate = NSPredicate(format: "%K == %@ AND %K != %@ AND (%K == %@ OR %K == %@)", Cheat.Attributes.game.rawValue, cheat.game, Cheat.Attributes.identifier.rawValue, cheat.identifier, Cheat.Attributes.code.rawValue, code, Cheat.Attributes.name.rawValue, name)
|
||||
|
||||
let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: self.managedObjectContext, type: Cheat.self)
|
||||
for cheat in cheats
|
||||
{
|
||||
if cheat.name == name
|
||||
{
|
||||
throw ValidationError.duplicateName
|
||||
}
|
||||
else if cheat.code == code
|
||||
{
|
||||
throw ValidationError.duplicateCode
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try self.delegate?.editCheatViewController(self, activateCheat: cheat, previousCheat: self.cheat)
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw ValidationError.invalidCode
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func textFieldDidEndEditing(sender: UITextField)
|
||||
{
|
||||
sender.resignFirstResponder()
|
||||
}
|
||||
|
||||
func presentErrorAlert(title title: String, message: String, handler: (Void -> Void)?)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .Default, handler: { action in
|
||||
handler?()
|
||||
}))
|
||||
self.presentViewController(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EditCheatViewController
|
||||
{
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
switch Section(rawValue: section)!
|
||||
{
|
||||
case .name: return super.tableView(tableView, titleForFooterInSection: section)
|
||||
|
||||
case .type:
|
||||
let title = String.localizedStringWithFormat("Code format is %@.", self.selectedCheatFormat.format)
|
||||
return title
|
||||
|
||||
case .code:
|
||||
let containsSpaces = self.selectedCheatFormat.format.containsString(" ")
|
||||
let containsDashes = self.selectedCheatFormat.format.containsString("-")
|
||||
|
||||
switch (containsSpaces, containsDashes)
|
||||
{
|
||||
case (true, false): return NSLocalizedString("Spaces will be inserted automatically as you type.", comment: "")
|
||||
case (false, true): return NSLocalizedString("Dashes will be inserted automatically as you type.", comment: "")
|
||||
case (true, true): return NSLocalizedString("Spaces and dashes will be inserted automatically as you type.", comment: "")
|
||||
case (false, false): return NSLocalizedString("Code will be formatted automatically as you type.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EditCheatViewController: UITextViewDelegate
|
||||
{
|
||||
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool
|
||||
{
|
||||
defer { self.updateSaveButtonState() }
|
||||
|
||||
guard text != "\n" else
|
||||
{
|
||||
textView.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
let sanitizedText = text.sanitized(characterSet: self.selectedCheatFormat.allowedCodeCharacters)
|
||||
|
||||
guard sanitizedText != text else { return true }
|
||||
|
||||
textView.textStorage.replaceCharactersInRange(range, withString: sanitizedText)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
2
External/Roxas
vendored
2
External/Roxas
vendored
@ -1 +1 @@
|
||||
Subproject commit c795d0ace22cfe57c3b73613630e14b58d2f8f09
|
||||
Subproject commit 17cc7b771ca819cdad14c05ee9cd0529d5df1c91
|
||||
Loading…
Reference in New Issue
Block a user