chore: align ark-suac app and refresh appstore zip

This commit is contained in:
Joachim Friberg
2026-03-23 21:23:16 +01:00
parent 9c4265b429
commit 9c5ea400fb
36 changed files with 1611 additions and 3 deletions
@@ -0,0 +1,4 @@
source 'https://rubygems.org'
gem 'slop', '= 4.10.1'
gem 'iniparse', '= 1.5.0'
@@ -0,0 +1,15 @@
GEM
remote: https://rubygems.org/
specs:
iniparse (1.5.0)
slop (4.10.1)
PLATFORMS
x86_64-linux-gnu
DEPENDENCIES
iniparse (= 1.5.0)
slop (= 4.10.1)
BUNDLED WITH
2.5.0.dev
@@ -0,0 +1,15 @@
module AsaCtrl
module Cli
class CliInterface
def initialize(opts)
@opts = opts
print_help! if opts[:help]
end
def print_help!
raise "Help not implemented!"
end
end
end
end
@@ -0,0 +1,34 @@
module AsaCtrl
module Cli
class ModsInterface < CliInterface
def initialize(opts)
super(opts)
execute!
end
def execute!
if @opts[:enable]
enable_mod!
end
exit! AsaCtrl::ExitCodes::OK
end
def enable_mod!
mod_id = @opts[:enable]
AsaCtrl::Mods::Database.get_instance.enable_mod!(mod_id)
puts "Enabled mod id '#{mod_id}' successfully. The server will download the mod upon startup."
rescue AsaCtrl::Errors::ModAlreadyEnabledError
AsaCtrl::Cli.exit_with_error!("This mod is already enabled! Use 'asa-ctrl mods --list' to see what mods are currently enabled.",
AsaCtrl::ExitCodes::MOD_ALREADY_ENABLED)
end
def print_help!
puts "Usage: asa-ctrl mods [--install] (--dry-run)"
exit! AsaCtrl::ExitCodes::OK
end
end
end
end
@@ -0,0 +1,42 @@
module AsaCtrl
module Cli
class RconInterface < CliInterface
def initialize(opts)
super(opts)
execute!
end
def execute!
if @opts[:exec]
run_command!
end
exit! AsaCtrl::ExitCodes::OK
end
def run_command!
rcon_command = @opts[:exec]
response = AsaCtrl::Rcon.exec_command!('127.0.0.1', AsaCtrl::Rcon.identify_port, rcon_command, AsaCtrl::Rcon.identify_password)
if response[:id] == AsaCtrl::Rcon::PacketTypes::RESPONSE_VALUE
puts response[:body]
else
AsaCtrl::Cli.exit_with_error!("Rcon command execution failed: #{response}",
AsaCtrl::ExitCodes::RCON_COMMAND_EXECUTION_FAILED)
end
rescue AsaCtrl::Errors::RconPasswordNotFoundError
AsaCtrl::Cli.exit_with_error!("Could not read RCON password. Make sure it is properly configured, either as start parameter ?ServerAdminPassword=mypass or " \
"in GameUserSettings.ini in the [ServerSettings] section as ServerAdminPassword=mypass", AsaCtrl::ExitCodes::RCON_PASSWORD_NOT_FOUND)
rescue AsaCtrl::Errors::RconAuthenticationError
AsaCtrl::Cli.exit_with_error!("Could not execute this RCON command. Authentication failed (wrong server password).",
AsaCtrl::ExitCodes::RCON_PASSWORD_WRONG)
end
def print_help!
puts "Usage: asa-ctrl rcon [--exec]"
exit! AsaCtrl::ExitCodes::OK
end
end
end
end
@@ -0,0 +1,23 @@
module AsaCtrl
module Cli
HELP_ARGUMENT = '--help'
HELP_DESCRIPTION = 'Prints a help message'
def self.passed_command(args)
if ARGV.size == 0
[]
else
[ARGV[0]]
end
end
def self.print_usage
puts "Usage: asa-ctrl [rcon] (--help)"
end
def self.exit_with_error!(message, code)
$stderr.puts "Error: #{message}"
exit! code
end
end
end
@@ -0,0 +1,5 @@
module AsaCtrl
module Errors
class BaseError < StandardError; end
end
end
@@ -0,0 +1,5 @@
require_relative './base_error.rb'
require_relative './mod_already_enabled_error.rb'
require_relative './rcon_authentication_error.rb'
require_relative './rcon_password_not_found_error.rb'
require_relative './rcon_port_not_found_error.rb'
@@ -0,0 +1,5 @@
module AsaCtrl
module Errors
class ModAlreadyEnabledError < BaseError; end
end
end
@@ -0,0 +1,5 @@
module AsaCtrl
module Errors
class RconAuthenticationError < BaseError; end
end
end
@@ -0,0 +1,5 @@
module AsaCtrl
module Errors
class RconPasswordNotFoundError < BaseError; end
end
end
@@ -0,0 +1,5 @@
module AsaCtrl
module Errors
class RconPortNotFoundError < BaseError; end
end
end
@@ -0,0 +1,10 @@
module AsaCtrl
module ExitCodes
OK = 0
CORRUPTED_MODS_DATABASE = 1
MOD_ALREADY_ENABLED = 2
RCON_PASSWORD_NOT_FOUND = 3
RCON_PASSWORD_WRONG = 4
RCON_COMMAND_EXECUTION_FAILED = 5
end
end
@@ -0,0 +1,2 @@
require_relative './start_params_helper.rb'
require_relative './ini_config_helper.rb'
@@ -0,0 +1,17 @@
module AsaCtrl
module IniConfigHelper
def self.game_user_settings_ini
self.parse('/home/gameserver/server-files/ShooterGame/Saved/Config/WindowsServer/GameUserSettings.ini')
end
def self.game_ini
self.parse('/home/gameserver/server-files/ShooterGame/Saved/Config/WindowsServer/Game.ini')
end
def self.parse(path)
return unless File.exist?(path)
IniParse.parse(File.read(path))
end
end
end
@@ -0,0 +1,22 @@
module AsaCtrl
module StartParamsHelper
def self.get_value(start_params, key)
return unless start_params
value = ''
offset = start_params.index("#{key}=")
return unless offset
offset += "#{key}=".length
start_params[offset..-1].each_char do |char|
break if char == ' ' || char == '?'
value += char
end
value
end
end
end
@@ -0,0 +1,36 @@
#!/usr/bin/ruby.ruby3.4
require 'json'
require 'slop'
require 'iniparse'
require 'socket'
if ENV['DEV'] == '1'
require 'byebug'
end
require_relative './exit_codes.rb'
require_relative './errors/errors.rb'
require_relative './helpers/helpers.rb'
require_relative './mods/database.rb'
require_relative './rcon/rcon.rb'
require_relative './cli/utils.rb'
require_relative './cli/interfaces/cli_interface.rb'
require_relative './cli/interfaces/mods_interface.rb'
require_relative './cli/interfaces/rcon_interface.rb'
main_args = Slop.parse(AsaCtrl::Cli.passed_command(ARGV)) do |args|
args.on 'rcon', 'Interface for RCON command execution' do
opts = Slop.parse(ARGV[1..-1]) do |opt|
opt.string '--exec', 'An RCON command to execute'
opt.bool AsaCtrl::Cli::HELP_ARGUMENT, AsaCtrl::Cli::HELP_DESCRIPTION
end
AsaCtrl::Cli::RconInterface.new(opts)
end
args.on AsaCtrl::Cli::HELP_ARGUMENT, AsaCtrl::Cli::HELP_DESCRIPTION do
# handled once slop exits
end
end
AsaCtrl::Cli.print_usage
@@ -0,0 +1,66 @@
module AsaCtrl
module Mods
MOD_DATABASE_PATH = '/home/gameserver/server-files/mods.json'
class Database
@@singleton_reference = nil
def initialize(database_path)
@database_path = database_path
ensure_database_presence!
load_database
end
def self.get_instance
return @@singleton_reference if @@singleton_reference
@@singleton_reference = Database.new(MOD_DATABASE_PATH)
end
def enable_mod!(mod_id)
@database.each do |record|
if record['mod_id'].to_i == mod_id.to_i
raise AsaCtrl::Errors::ModAlreadyEnabledError if record['enabled']
record['enabled'] = true
write_database!
return
end
end
add_new_record!(mod_id, 'unknown', true, false)
end
def add_new_record!(mod_id, name, enabled, scanned)
@database << {
mod_id: mod_id.to_i,
name: name,
enabled: enabled,
scanned: scanned
}
write_database!
end
def write_database!
File.write(@database_path, JSON.pretty_generate(@database))
end
def ensure_database_presence!
return if File.exist?(@database_path)
@database = []
write_database!
end
def load_database
@database = JSON.parse(File.read(@database_path))
rescue JSON::ParserError
# we do not want to delete the file for the user, as they might want to save its content first
AsaCtrl::Cli.exit_with_error!("mods.json file is corrupted and cannot be parsed, please delete this file " \
"manually. It can be found in the server files root directory.", AsaCtrl::ExitCodes::CORRUPTED_MODS_DATABASE)
end
end
end
end
@@ -0,0 +1,69 @@
module AsaCtrl
module Rcon
module PacketTypes
RESPONSE_VALUE = 0
EXEC_COMMAND = 2
AUTH_RESPONSE = 2
AUTH = 3
end
Packet = Struct.new(:size, :id, :type, :body)
def self.exec_command!(server_ip, rcon_port, rcon_command, password)
socket = TCPSocket.new(server_ip, rcon_port)
raise AsaCtrl::Errors::RconAuthenticationError unless self.authenticate!(socket, password)
self.send_packet!(socket, rcon_command, PacketTypes::EXEC_COMMAND)
end
def self.authenticate!(socket, password)
response = self.send_packet!(socket, password, PacketTypes::AUTH)
response[:id] != -1
end
def self.send_packet!(socket, data, packet_id)
packet = Packet.new(10+data.bytesize, 0, packet_id, data)
self.send_to(packet, socket)
self.recv_from(socket)
end
def self.send_to(packet, socket)
szb = [packet[:size]].pack 'l<'
idb = [packet[:id]].pack 'l<'
type_b = [packet[:type]].pack 'l<'
body_b = [packet[:body]].pack 'Z*'
data = szb + idb + type_b + body_b + "\0"
socket.sendmsg(data)
end
def self.recv_from(socket)
msg_ary = socket.recvmsg
msg = msg_ary[0]
ary = msg.unpack('l<l<l<Z*')
Packet.new(ary[0], ary[1], ary[2], ary[3])
end
def self.identify_password
password = AsaCtrl::StartParamsHelper.get_value(ENV['ASA_START_PARAMS'], 'ServerAdminPassword')
return password if password
config = AsaCtrl::IniConfigHelper.game_user_settings_ini
return config['ServerSettings']['ServerAdminPassword'] if config['ServerSettings'] && config['ServerSettings']['ServerAdminPassword']
raise AsaCtrl::Errors::RconPasswordNotFoundError
end
def self.identify_port
port = AsaCtrl::StartParamsHelper.get_value(ENV['ASA_START_PARAMS'], 'RCONPort')
return port.to_i if port
config = AsaCtrl::IniConfigHelper.game_user_settings_ini
return config['ServerSettings']['RCONPort'].to_i if config['ServerSettings'] && config['ServerSettings']['RCONPort']
raise AsaCtrl::Errors::RconPortNotFoundError
end
end
end