mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
Merge pull request #579 from castdrian/metal
feat: metal support and make ios usable
This commit is contained in:
@@ -49,7 +49,7 @@ function love.conf(t)
|
|||||||
t.window.minwidth = 480
|
t.window.minwidth = 480
|
||||||
t.window.minheight = 360
|
t.window.minheight = 360
|
||||||
end
|
end
|
||||||
t.version = "11.5"
|
t.version = love._os == "iOS" and "12.0" or "11.5"
|
||||||
t.window.vsync = 1
|
t.window.vsync = 1
|
||||||
t.modules.joystick = true
|
t.modules.joystick = true
|
||||||
t.modules.physics = false
|
t.modules.physics = false
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Pokemon Red</string>
|
<string>gen1recomp</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -49,10 +49,6 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>Launch Screen</string>
|
<string>Launch Screen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
|
||||||
<array>
|
|
||||||
<string>opengles-2</string>
|
|
||||||
</array>
|
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ TARGET="all"
|
|||||||
NOTARY_PROFILE="notary-profile"
|
NOTARY_PROFILE="notary-profile"
|
||||||
NOTARIZE=true
|
NOTARIZE=true
|
||||||
IOS_RELEASE=false
|
IOS_RELEASE=false
|
||||||
|
IOS_IPA=false
|
||||||
|
|
||||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||||
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
|
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
|
||||||
@@ -48,6 +49,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--notary-profile) NOTARY_PROFILE="$2"; shift ;;
|
--notary-profile) NOTARY_PROFILE="$2"; shift ;;
|
||||||
--no-notarize) NOTARIZE=false ;;
|
--no-notarize) NOTARIZE=false ;;
|
||||||
--release) IOS_RELEASE=true ;;
|
--release) IOS_RELEASE=true ;;
|
||||||
|
--ipa) IOS_IPA=true ;;
|
||||||
*) fail "unknown argument: $1" ;;
|
*) fail "unknown argument: $1" ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -298,6 +300,9 @@ build_android() {
|
|||||||
if [ "$VERSION_EXPLICIT" = true ]; then
|
if [ "$VERSION_EXPLICIT" = true ]; then
|
||||||
args+=(--version "$VERSION")
|
args+=(--version "$VERSION")
|
||||||
fi
|
fi
|
||||||
|
if [ "$IOS_IPA" = true ]; then
|
||||||
|
args+=(--ipa)
|
||||||
|
fi
|
||||||
"$ROOT/scripts/build_android.sh" ${args[@]+"${args[@]}"}
|
"$ROOT/scripts/build_android.sh" ${args[@]+"${args[@]}"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-3
@@ -50,7 +50,7 @@ DISPLAY_NAME="gen1recomp"
|
|||||||
# capabilities like HealthKit are involved), so a per-team default
|
# capabilities like HealthKit are involved), so a per-team default
|
||||||
# lets anyone build without colliding with someone else's app
|
# lets anyone build without colliding with someone else's app
|
||||||
# 4. simulator: the project default (no App ID registration involved)
|
# 4. simulator: the project default (no App ID registration involved)
|
||||||
BUNDLE_ID="${GEN1_BUNDLE_ID:-}"
|
BUNDLE_ID="${GEN1_BUNDLE_ID:-com.theboisclub.gen1recomp}"
|
||||||
if [ -z "$BUNDLE_ID" ] && [ -f "$IOS_DIR/bundle_id.local" ]; then
|
if [ -z "$BUNDLE_ID" ] && [ -f "$IOS_DIR/bundle_id.local" ]; then
|
||||||
BUNDLE_ID="$(tr -d '[:space:]' < "$IOS_DIR/bundle_id.local")"
|
BUNDLE_ID="$(tr -d '[:space:]' < "$IOS_DIR/bundle_id.local")"
|
||||||
fi
|
fi
|
||||||
@@ -65,6 +65,7 @@ DEVICE=false
|
|||||||
RELEASE=false
|
RELEASE=false
|
||||||
PACKAGE_ONLY=false
|
PACKAGE_ONLY=false
|
||||||
INSTALL=false
|
INSTALL=false
|
||||||
|
CREATE_IPA=false
|
||||||
# Last resort for an incomplete source export, mirroring build_android.sh.
|
# Last resort for an incomplete source export, mirroring build_android.sh.
|
||||||
MANIFEST_BASE_URL="${MANIFEST_BASE_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main}"
|
MANIFEST_BASE_URL="${MANIFEST_BASE_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main}"
|
||||||
MANIFESTS=""
|
MANIFESTS=""
|
||||||
@@ -82,6 +83,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--release) RELEASE=true ;;
|
--release) RELEASE=true ;;
|
||||||
--package-only) PACKAGE_ONLY=true ;;
|
--package-only) PACKAGE_ONLY=true ;;
|
||||||
--install) INSTALL=true ;;
|
--install) INSTALL=true ;;
|
||||||
|
--ipa) CREATE_IPA=true ;;
|
||||||
--version) VERSION="$2"; shift ;;
|
--version) VERSION="$2"; shift ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
sed -n '2,24p' "$0"
|
sed -n '2,24p' "$0"
|
||||||
@@ -92,6 +94,10 @@ while [ $# -gt 0 ]; do
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if $CREATE_IPA; then
|
||||||
|
DEVICE=true
|
||||||
|
fi
|
||||||
|
|
||||||
VERSION_CODE=""
|
VERSION_CODE=""
|
||||||
if [ -n "$VERSION" ]; then
|
if [ -n "$VERSION" ]; then
|
||||||
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
@@ -232,6 +238,30 @@ apply_ios_branding() {
|
|||||||
cp "$OVERLAY_PLIST" "$dest"
|
cp "$OVERLAY_PLIST" "$dest"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply_ios_icon() {
|
||||||
|
local source="$ROOT/assets/logo/logo.png"
|
||||||
|
local target="$XCODE_DIR/Images.xcassets/iOS AppIcon.appiconset"
|
||||||
|
[ -f "$source" ] || fail "missing iOS icon source: $source"
|
||||||
|
[ -d "$target" ] || fail "missing iOS app icon set: $target"
|
||||||
|
local entry name size
|
||||||
|
while IFS=: read -r name size; do
|
||||||
|
sips -z "$size" "$size" "$source" --out "$target/$name" >/dev/null
|
||||||
|
done <<'EOF'
|
||||||
|
icon-1024pt@1x.png:1024
|
||||||
|
icon-29pt@1x.png:29
|
||||||
|
icon-29pt@2x.png:58
|
||||||
|
icon-29pt@3x.png:87
|
||||||
|
icon-40pt@1x.png:40
|
||||||
|
icon-40pt@2x.png:80
|
||||||
|
icon-40pt@3x.png:120
|
||||||
|
icon-60pt@2x.png:120
|
||||||
|
icon-60pt@3x.png:180
|
||||||
|
icon-76pt@1x.png:76
|
||||||
|
icon-76pt@2x.png:152
|
||||||
|
icon-83.5pt@2x.png:167
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------- game.love
|
# --------------------------------------------------------------- game.love
|
||||||
# Every version's import manifest has to ship or that game's ROM import fails in
|
# Every version's import manifest has to ship or that game's ROM import fails in
|
||||||
# the built app: decodeManifest (src/import/RomImporter.lua) errors outright when
|
# the built app: decodeManifest (src/import/RomImporter.lua) errors outright when
|
||||||
@@ -326,8 +356,9 @@ pack_game_love() {
|
|||||||
# in the built app while dev, which reads the source tree, stayed green.
|
# in the built app while dev, which reads the source tree, stayed green.
|
||||||
archive_entries="$(unzip -Z1 "$LOVE_FILE")"
|
archive_entries="$(unzip -Z1 "$LOVE_FILE")"
|
||||||
# shellcheck disable=SC2086 # MANIFESTS is a deliberate word list
|
# shellcheck disable=SC2086 # MANIFESTS is a deliberate word list
|
||||||
for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \
|
for required in src/update/Boot.lua tools/save-editor/App.lua \
|
||||||
tools/save-editor/panels/Party.lua $MANIFESTS; do
|
tools/save-editor/Kit.lua tools/save-editor/panels/Party.lua \
|
||||||
|
$MANIFESTS; do
|
||||||
printf '%s\n' "$archive_entries" | grep -qx "$required" \
|
printf '%s\n' "$archive_entries" | grep -qx "$required" \
|
||||||
|| fail "game.love is missing $required"
|
|| fail "game.love is missing $required"
|
||||||
done
|
done
|
||||||
@@ -640,6 +671,7 @@ install_to_device() {
|
|||||||
|
|
||||||
# --------------------------------------------------------------- main
|
# --------------------------------------------------------------- main
|
||||||
apply_ios_branding
|
apply_ios_branding
|
||||||
|
apply_ios_icon
|
||||||
say "applying iOS native bridge patches (picker/Files support)"
|
say "applying iOS native bridge patches (picker/Files support)"
|
||||||
python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed"
|
python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed"
|
||||||
ensure_manifests
|
ensure_manifests
|
||||||
|
|||||||
@@ -648,6 +648,8 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- Android SAF create-document: which game's SAVE FILES card should show
|
-- Android SAF create-document: which game's SAVE FILES card should show
|
||||||
-- "Save exported." when export_done.flag appears on focus.
|
-- "Save exported." when export_done.flag appears on focus.
|
||||||
androidPendingExportVersion = nil,
|
androidPendingExportVersion = nil,
|
||||||
|
iosPendingKind = nil,
|
||||||
|
iosPendingVersion = nil,
|
||||||
-- Virtual pointer for handhelds / gamepads (Anbernic stock OS has no
|
-- Virtual pointer for handhelds / gamepads (Anbernic stock OS has no
|
||||||
-- mouse). D-pad + left stick move it; A clicks; shoulders cycle tabs;
|
-- mouse). D-pad + left stick move it; A clicks; shoulders cycle tabs;
|
||||||
-- right stick scrolls the save-slot / mods lists.
|
-- right stick scrolls the save-slot / mods lists.
|
||||||
@@ -829,7 +831,11 @@ local function resetPointerCursor(self)
|
|||||||
if not (love.mouse.isCursorSupported and love.mouse.isCursorSupported()) then
|
if not (love.mouse.isCursorSupported and love.mouse.isCursorSupported()) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.arrowCursor = self.arrowCursor or love.mouse.getSystemCursor("arrow")
|
if not self.arrowCursor then
|
||||||
|
local ok, cursor = pcall(love.mouse.getSystemCursor, "arrow")
|
||||||
|
if not ok then return end
|
||||||
|
self.arrowCursor = cursor
|
||||||
|
end
|
||||||
love.mouse.setCursor(self.arrowCursor)
|
love.mouse.setCursor(self.arrowCursor)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1015,6 +1021,14 @@ end
|
|||||||
-- which focus/Choose consumes on return.
|
-- which focus/Choose consumes on return.
|
||||||
function RomImporter:chooseMod()
|
function RomImporter:chooseMod()
|
||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
|
if self.ios and love.system.getPickedFile then
|
||||||
|
self.iosPendingKind = "mod"
|
||||||
|
if not pickFile("mod") then
|
||||||
|
self.iosPendingKind = nil
|
||||||
|
self.modNotice = { ok = false, text = "Could not open the file picker." }
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.android then
|
if self.android then
|
||||||
local name = findPendingMod(true, self.pickSkip)
|
local name = findPendingMod(true, self.pickSkip)
|
||||||
if name then
|
if name then
|
||||||
@@ -1076,6 +1090,16 @@ end
|
|||||||
-- Android mirrors ROM / mod import via love.system.pickFile("sav").
|
-- Android mirrors ROM / mod import via love.system.pickFile("sav").
|
||||||
function RomImporter:chooseSaveImport(version)
|
function RomImporter:chooseSaveImport(version)
|
||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
|
if self.ios and love.system.getPickedFile then
|
||||||
|
self.iosPendingKind = "sav"
|
||||||
|
self.iosPendingVersion = version
|
||||||
|
if not pickFile("sav") then
|
||||||
|
self.iosPendingKind = nil
|
||||||
|
self.iosPendingVersion = nil
|
||||||
|
self.saveNotice[version] = { ok = false, text = "Could not open the file picker." }
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.android then
|
if self.android then
|
||||||
local name = findPendingSav(true, self.pickSkip)
|
local name = findPendingSav(true, self.pickSkip)
|
||||||
if name then
|
if name then
|
||||||
@@ -1128,7 +1152,7 @@ function RomImporter:exportSave(version)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.androidPendingExportVersion = version
|
self.androidPendingExportVersion = version
|
||||||
if love.system.createFile and love.system.createFile(suggested) then
|
if love.system.createFile and love.system.createFile(suggested, love.filesystem.getSaveDirectory()) then
|
||||||
self.pickPending = true
|
self.pickPending = true
|
||||||
self.pickTimer = 0
|
self.pickTimer = 0
|
||||||
self.saveNotice[version] = { ok = true,
|
self.saveNotice[version] = { ok = true,
|
||||||
@@ -1165,6 +1189,14 @@ end
|
|||||||
function RomImporter:choose(version)
|
function RomImporter:choose(version)
|
||||||
if self.workState == "working" then return end
|
if self.workState == "working" then return end
|
||||||
self.chooseVersion = version or "red"
|
self.chooseVersion = version or "red"
|
||||||
|
if self.ios and love.system.getPickedFile then
|
||||||
|
self.iosPendingKind = "rom"
|
||||||
|
if not pickFile("rom") then
|
||||||
|
self.iosPendingKind = nil
|
||||||
|
self:setError("Could not open the file picker.")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.android then
|
if self.android then
|
||||||
-- Prefer a not-yet-imported .gb/.gbc already in the save dir (USB copy, or
|
-- Prefer a not-yet-imported .gb/.gbc already in the save dir (USB copy, or
|
||||||
-- a fresh SAF pick). Never reuse an already-imported cart's file -- that
|
-- a fresh SAF pick). Never reuse an already-imported cart's file -- that
|
||||||
@@ -1279,6 +1311,37 @@ end
|
|||||||
function RomImporter:update(dt)
|
function RomImporter:update(dt)
|
||||||
self.pulse = self.pulse + dt
|
self.pulse = self.pulse + dt
|
||||||
self:_updatePadCursor(dt)
|
self:_updatePadCursor(dt)
|
||||||
|
if self.ios and love.system.getPickedFile and self.workState ~= "working" then
|
||||||
|
local path = love.system.getPickedFile()
|
||||||
|
if path then
|
||||||
|
local kind = self.iosPendingKind or "rom"
|
||||||
|
local version = self.iosPendingVersion
|
||||||
|
self.iosPendingKind = nil
|
||||||
|
self.iosPendingVersion = nil
|
||||||
|
if kind == "mod" then
|
||||||
|
self:_installMod(path)
|
||||||
|
elseif kind == "sav" then
|
||||||
|
self:_importSave(version or self:_savedropTarget(), path)
|
||||||
|
else
|
||||||
|
self:startPath(path)
|
||||||
|
end
|
||||||
|
elseif love.system.getPickError then
|
||||||
|
local errorText = love.system.getPickError()
|
||||||
|
if errorText then
|
||||||
|
local kind = self.iosPendingKind or "rom"
|
||||||
|
local version = self.iosPendingVersion or self:_savedropTarget()
|
||||||
|
self.iosPendingKind = nil
|
||||||
|
self.iosPendingVersion = nil
|
||||||
|
if kind == "mod" then
|
||||||
|
self.modNotice = { ok = false, text = errorText }
|
||||||
|
elseif kind == "sav" then
|
||||||
|
self.saveNotice[version] = { ok = false, text = errorText }
|
||||||
|
else
|
||||||
|
self:setError(errorText)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
self:_pollPickedFiles(dt)
|
self:_pollPickedFiles(dt)
|
||||||
if self.workState ~= "working" or not self.worker then return end
|
if self.workState ~= "working" or not self.worker then return end
|
||||||
local started = love.timer.getTime()
|
local started = love.timer.getTime()
|
||||||
@@ -2523,8 +2586,11 @@ function RomImporter:draw()
|
|||||||
if self._hoverEnabled and not self._padCursorActive
|
if self._hoverEnabled and not self._padCursorActive
|
||||||
and love.mouse.isCursorSupported and love.mouse.isCursorSupported() then
|
and love.mouse.isCursorSupported and love.mouse.isCursorSupported() then
|
||||||
if self._anyHover then
|
if self._anyHover then
|
||||||
self.handCursor = self.handCursor or love.mouse.getSystemCursor("hand")
|
if not self.handCursor then
|
||||||
love.mouse.setCursor(self.handCursor)
|
local ok, cursor = pcall(love.mouse.getSystemCursor, "hand")
|
||||||
|
if ok then self.handCursor = cursor end
|
||||||
|
end
|
||||||
|
if self.handCursor then love.mouse.setCursor(self.handCursor) end
|
||||||
else
|
else
|
||||||
resetPointerCursor(self)
|
resetPointerCursor(self)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,4 +36,13 @@ ri:play("red")
|
|||||||
eq(booted, "red", "android play still boots")
|
eq(booted, "red", "android play still boots")
|
||||||
eq(currentCursor, "hand", "android play leaves the cursor alone")
|
eq(currentCursor, "hand", "android play leaves the cursor alone")
|
||||||
|
|
||||||
|
booted = nil
|
||||||
|
currentCursor = "hand"
|
||||||
|
ri.android = false
|
||||||
|
ri.arrowCursor = nil
|
||||||
|
love.mouse.getSystemCursor = function() error("CreateSystemCursor is not currently supported") end
|
||||||
|
ri:play("red")
|
||||||
|
eq(booted, "red", "unsupported system cursors still allow boot")
|
||||||
|
eq(currentCursor, "hand", "unsupported system cursors leave the existing cursor alone")
|
||||||
|
|
||||||
S.finish()
|
S.finish()
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
|
||||||
|
local S = require("tests.harness").suite("rom importer iOS save export")
|
||||||
|
local eq = S.eq
|
||||||
|
local check = S.check
|
||||||
|
|
||||||
|
love.system = love.system or {}
|
||||||
|
local saved = {
|
||||||
|
createFile = love.system.createFile,
|
||||||
|
read = love.filesystem.read,
|
||||||
|
write = love.filesystem.write,
|
||||||
|
getSaveDirectory = love.filesystem.getSaveDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
local writes = {}
|
||||||
|
love.filesystem.read = function(name)
|
||||||
|
eq(name, "exports/red.sav", "export reads the generated save")
|
||||||
|
return "save-data"
|
||||||
|
end
|
||||||
|
love.filesystem.write = function(name, data)
|
||||||
|
writes[#writes + 1] = { name = name, data = data }
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
love.filesystem.getSaveDirectory = function() return "/tmp/gen1recomp-save" end
|
||||||
|
|
||||||
|
local createCalls = {}
|
||||||
|
love.system.createFile = function(name, saveDirectory)
|
||||||
|
createCalls[#createCalls + 1] = { name = name, saveDirectory = saveDirectory }
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
package.loaded["src.import.SaveFileIO"] = {
|
||||||
|
exportActiveSlot = function(version)
|
||||||
|
eq(version, "red", "exports the selected game save")
|
||||||
|
return true, "exports/red.sav"
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
local ri = setmetatable({
|
||||||
|
android = true,
|
||||||
|
ios = true,
|
||||||
|
workState = nil,
|
||||||
|
saveNotice = {},
|
||||||
|
}, RomImporter)
|
||||||
|
|
||||||
|
ri:exportSave("red")
|
||||||
|
|
||||||
|
eq(#writes, 1, "stages the save for iOS export")
|
||||||
|
eq(writes[1].name, "pending_export.sav", "uses the native bridge staging name")
|
||||||
|
eq(writes[1].data, "save-data", "preserves the exported save data")
|
||||||
|
eq(#createCalls, 1, "opens the system destination picker")
|
||||||
|
eq(createCalls[1].name, "red.sav", "suggests the original save name")
|
||||||
|
eq(createCalls[1].saveDirectory, "/tmp/gen1recomp-save", "passes the exact staging directory")
|
||||||
|
check(ri.saveNotice.red.ok, "reports that the destination picker opened")
|
||||||
|
|
||||||
|
love.system.createFile = saved.createFile
|
||||||
|
love.filesystem.read = saved.read
|
||||||
|
love.filesystem.write = saved.write
|
||||||
|
love.filesystem.getSaveDirectory = saved.getSaveDirectory
|
||||||
|
package.loaded["src.import.SaveFileIO"] = nil
|
||||||
|
|
||||||
|
S.finish()
|
||||||
Reference in New Issue
Block a user