diff --git a/src/crystal-gauntlet.cr b/src/crystal-gauntlet.cr index 4da5c10..697c7c6 100644 --- a/src/crystal-gauntlet.cr +++ b/src/crystal-gauntlet.cr @@ -26,10 +26,14 @@ require "./lib/creator_points" require "./lib/versions" require "./lib/ips" +require "./patch-exe.cr" + if File.exists?(".env") Dotenv.load end +include CrystalGauntlet::PatchExe + module CrystalGauntlet VERSION = "0.1.0" @@ -211,6 +215,8 @@ module CrystalGauntlet migrate = false calc_creator_points = false + patch_exe = false + patch_exe_location = nil parser = OptionParser.new do |parser| parser.banner = "Usage: crystal-gauntlet [command] [arguments]" @@ -223,6 +229,13 @@ module CrystalGauntlet calc_creator_points = true parser.banner = "Usage: crystal-gauntlet calc_creator_points [arguments]" end + parser.on("patch_exe", "Patch Geometry Dash executables with your server URL (supports #{SUPPORTED_PATCH_PLATFORMS.join(", ")})") do + patch_exe = true + parser.banner = "Usage: crystal-gauntlet patch_exe " + parser.unknown_args do |opt| + patch_exe_location = opt[0]? + end + end parser.on("-h", "--help", "Show this help") do puts parser exit @@ -231,6 +244,17 @@ module CrystalGauntlet parser.parse + if patch_exe + if !patch_exe_location + puts parser + exit 1 + end + check_server_length(true) + LOG.info { "Patching #{patch_exe_location}" } + patch_exe_file(patch_exe_location.not_nil!) + exit + end + migrator = Migrate::Migrator.new( DATABASE ) @@ -238,7 +262,10 @@ module CrystalGauntlet if migrate LOG.info { "Migrating #{ENV["DATABASE_URL"].colorize(:white)}..." } migrator.to_latest - elsif calc_creator_points + exit + end + + if calc_creator_points LOG.info { "updating creator points" } DATABASE.query_all("select id, username, creator_points from users", as: {Int32, String, Int32}).each() do |id, username, old_count| new_count = CreatorPoints.update_creator_points id @@ -246,50 +273,43 @@ module CrystalGauntlet LOG.info { "#{username}: #{old_count} -> #{new_count}" } end end - else - if !migrator.latest? - LOG.fatal { "Database hasn\'t been migrated!! Please run #{"crystal-gauntlet migrate".colorize(:white)}" } - return - end - ["songs", "levels", "saves"].each() { |v| - Dir.mkdir_p(DATA_FOLDER / v) - } - - server = HTTP::Server.new([ - HTTP::LogHandler.new, - HTTP::StaticFileHandler.new("public/", fallthrough: true, directory_listing: false), - HTTP::StaticFileHandler.new((DATA_FOLDER / "songs").to_s, fallthrough: true, directory_listing: false), - CrystalGauntlet::GDHandler.new, - CrystalGauntlet::TemplateHandler.new - ]) - - listen_on = URI.parse(ENV["LISTEN_ON"]? || "http://localhost:8080").normalize - - case listen_on.scheme - when "http" - server.bind_tcp(listen_on.hostname.not_nil!, listen_on.port.not_nil!) - when "unix" - server.bind_unix(listen_on.to_s.sub("unix://","")) - end - - full_server_path = config_get("general.hostname", "") + "/" + config_get("general.append_path", "") - robtop_server_path = "www.boomlings.com/database/" - if full_server_path.size != robtop_server_path.size - LOG.warn { "i think you made a mistake? length of full server path and default .exe location do not match" } - LOG.warn { " #{full_server_path}" } - LOG.warn { " #{robtop_server_path}" } - min_length = Math.min(full_server_path.size, robtop_server_path.size) - max_length = Math.max(full_server_path.size, robtop_server_path.size) - LOG.warn { " #{" " * min_length}#{"^" * (max_length - min_length)}"} - end - - Reupload.init() - - @@up_at = Time.utc - LOG.notice { "Listening on #{listen_on.to_s.colorize(:white)}" } - server.listen + exit end + + if !migrator.latest? + LOG.fatal { "Database hasn\'t been migrated!! Please run #{"crystal-gauntlet migrate".colorize(:white)}" } + exit 1 + end + + ["songs", "levels", "saves"].each() { |v| + Dir.mkdir_p(DATA_FOLDER / v) + } + + server = HTTP::Server.new([ + HTTP::LogHandler.new, + HTTP::StaticFileHandler.new("public/", fallthrough: true, directory_listing: false), + HTTP::StaticFileHandler.new((DATA_FOLDER / "songs").to_s, fallthrough: true, directory_listing: false), + CrystalGauntlet::GDHandler.new, + CrystalGauntlet::TemplateHandler.new + ]) + + listen_on = URI.parse(ENV["LISTEN_ON"]? || "http://localhost:8080").normalize + + case listen_on.scheme + when "http" + server.bind_tcp(listen_on.hostname.not_nil!, listen_on.port.not_nil!) + when "unix" + server.bind_unix(listen_on.to_s.sub("unix://","")) + end + + check_server_length(false) + + Reupload.init() + + @@up_at = Time.utc + LOG.notice { "Listening on #{listen_on.to_s.colorize(:white)}" } + server.listen end end diff --git a/src/patch-exe.cr b/src/patch-exe.cr new file mode 100644 index 0000000..9514fa1 --- /dev/null +++ b/src/patch-exe.cr @@ -0,0 +1,96 @@ +module CrystalGauntlet::PatchExe + extend self + + SUPPORTED_PATCH_PLATFORMS = ["Windows"] + SUPPORTED_EXTENSIONS = ["exe"] + + ROBTOP_SERVER_PATH = "http://www.boomlings.com/database" + + def robtop_server_path + ROBTOP_SERVER_PATH + end + + def full_server_path + "http://" + config_get("general.hostname", "") + "/" + config_get("general.append_path", "").chomp("/") + end + + def replace(from : IO, to : IO, search : Array(UInt8), replace : Array(UInt8)) + if search.size != replace.size + raise "Search and replacement does not match in size" + end + + size = search.size + + buffer = [] of UInt8 + + replacements = 0 + + from.each_byte do |byte| + if buffer.size >= size + insert_byte = buffer.shift + to.write_byte(insert_byte) + end + + buffer << byte + + if buffer == search + replace.each() { |b| to.write_byte(b) } + buffer = [] of UInt8 + replacements += 1 + end + end + + buffer.each() { |b| to.write_byte(b) } + + replacements + end + + def patch_exe_file(location : String) + file_split = location.split(".") + extension = file_split.pop + patched_location = "#{file_split.join(".")}_patched.#{extension}" + File.open("#{location}", "r") do |from| + File.open(patched_location, "w") do |to| + start = Time.monotonic + + amt = 0 + + case extension + when "exe" + gd_temp = File.tempfile("GeometryDash") + LOG.debug { " #{robtop_server_path.colorize(:dark_gray)} ->" } + LOG.debug { " #{full_server_path.colorize(:dark_gray)}" } + File.open(gd_temp.path, "w") do |tmp| + amt += replace(from, tmp, robtop_server_path.bytes, full_server_path.bytes) + end + LOG.debug { " #{Base64.strict_encode(robtop_server_path).colorize(:dark_gray)} ->" } + LOG.debug { " #{Base64.strict_encode(full_server_path).colorize(:dark_gray)}" } + File.open(gd_temp.path, "r") do |tmp| + amt += replace(tmp, to, Base64.strict_encode(robtop_server_path).bytes, Base64.strict_encode(full_server_path).bytes) + end + gd_temp.delete + else + LOG.error { "Unsupported extension #{extension.colorize(:white)} (supported: #{SUPPORTED_EXTENSIONS.join(", ")})" } + end + + LOG.info { "Patched #{location} into #{patched_location} successfully" } + LOG.info { "#{amt} replacements done in #{(Time.monotonic - start).total_seconds.humanize(precision: 2, significant: false)}s" } + end + end + end + + def check_server_length(exit_if_fail : Bool) + if full_server_path.size != robtop_server_path.size + LOG.warn { "i think you made a mistake? length of full server path and default .exe location do not match" } + LOG.warn { " #{full_server_path}" } + LOG.warn { " #{robtop_server_path}" } + min_length = Math.min(full_server_path.size, robtop_server_path.size) + max_length = Math.max(full_server_path.size, robtop_server_path.size) + LOG.warn { " #{" " * min_length}#{"^" * (max_length - min_length)}"} + + if exit_if_fail + exit 1 + end + end + end +end