From 6285395adb1c76ecdcbcbf77e81536b57525b1dc Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Thu, 5 Jan 2023 15:57:26 +0300 Subject: [PATCH] quests --- config.example.toml | 63 ++++++++++++++++++++ db/migrations/13_quests.sql | 8 +++ src/crystal-gauntlet.cr | 3 +- src/endpoints/misc/getQuests.cr | 99 ++++++++++++++++++++++++++++++++ src/endpoints/misc/getRewards.cr | 17 +++--- src/lib/format.cr | 20 ------- src/lib/xor.cr | 22 +++++++ 7 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 db/migrations/13_quests.sql create mode 100644 src/endpoints/misc/getQuests.cr create mode 100644 src/lib/xor.cr diff --git a/config.example.toml b/config.example.toml index bbce848..5072481 100644 --- a/config.example.toml +++ b/config.example.toml @@ -59,6 +59,69 @@ shards_max = 2 keys_min = 0 keys_max = 1 +[quests] +enabled = true +# in seconds +timer = 21600 # 6hr + +# top slot quests +[[quests.tier_1]] +name = "Orb Finder" +required_type = "orb" +required_amt = 200 +reward_diamonds = 5 + +[[quests.tier_1]] +name = "Star Finder" +required_type = "star" +required_amt = 5 +reward_diamonds = 5 + +[[quests.tier_1]] +name = "Coin Finder" +required_type = "coin" +required_amt = 2 +reward_diamonds = 5 + +# middle slot quests +[[quests.tier_2]] +name = "Orb Collector" +required_type = "orb" +required_amt = 500 +reward_diamonds = 10 + +[[quests.tier_2]] +name = "Star Collector" +required_type = "star" +required_amt = 10 +reward_diamonds = 10 + +[[quests.tier_2]] +name = "Coin Collector" +required_type = "coin" +required_amt = 4 +reward_diamonds = 10 + +# bottom slot quests +[[quests.tier_3]] +name = "Orb Master" +required_type = "orb" +required_amt = 1000 +reward_diamonds = 15 + +[[quests.tier_3]] +name = "Star Master" +required_type = "star" +required_amt = 15 +reward_diamonds = 15 + +[[quests.tier_3]] +name = "Coin Master" +required_type = "coin" +required_amt = 6 +reward_diamonds = 15 + + [voting] # allow votes to influence a level's difficulty when it # hasn't been set yet. when set to false, all unrated diff --git a/db/migrations/13_quests.sql b/db/migrations/13_quests.sql new file mode 100644 index 0000000..7025c55 --- /dev/null +++ b/db/migrations/13_quests.sql @@ -0,0 +1,8 @@ +-- +migrate up +CREATE TABLE quest_timer ( + account_id INTEGER NOT NULL references accounts(id), + next_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')) +); + +-- +migrate down +DROP TABLE quest_timer; \ No newline at end of file diff --git a/src/crystal-gauntlet.cr b/src/crystal-gauntlet.cr index 2dde4ac..e1ed907 100644 --- a/src/crystal-gauntlet.cr +++ b/src/crystal-gauntlet.cr @@ -12,6 +12,7 @@ require "migrate" require "./enums" require "./lib/hash" require "./lib/format" +require "./lib/xor" require "./lib/accounts" require "./lib/gjp" require "./lib/clean" @@ -221,7 +222,7 @@ module CrystalGauntlet else if !migrator.latest? LOG.fatal { "Database hasn\'t been migrated!! Please run #{"crystal-gauntlet migrate".colorize(:white)}" } - Process.exit(1) + return end server = HTTP::Server.new([ diff --git a/src/endpoints/misc/getQuests.cr b/src/endpoints/misc/getQuests.cr new file mode 100644 index 0000000..b6bc087 --- /dev/null +++ b/src/endpoints/misc/getQuests.cr @@ -0,0 +1,99 @@ +require "uri" + +include CrystalGauntlet + +private PAD_STR = "_____" # meaningless but necessary + +private def get_chk_value(chk_str : String) + XorCrypt.encrypt_string(Base64.decode_string(chk_str[5..]), XorCrypt::QUESTS_XOR_KEY) +end + +private def get_quest_time(account_id : Int32) + timer = config_get("quests.timer").as?(Int64) || 0 + + begin + next_str = DATABASE.query_one("select next_at from quest_timer where account_id = ?", account_id, as: {String}) + rescue + next_at = (Time.utc + timer.seconds).to_s(Format::TIME_FORMAT) + DATABASE.exec("insert into quest_timer (account_id, next_at) values (?, ?)", account_id, next_at) + return 0 + else + next_at = Time.parse(next_str, Format::TIME_FORMAT, Time::Location::UTC) + seconds_left = (next_at - Time.utc).total_seconds.to_i + if seconds_left <= 0 + next_at = (Time.utc + timer.seconds).to_s(Format::TIME_FORMAT) + DATABASE.exec("update quest_timer set next_at = ? where account_id = ?", next_at, account_id) + end + + return seconds_left + end +end + +private def type_to_i(type : String?) + case type + when "orb", "orbs" + 1 + when "coin", "coins" + 2 + when "star", "stars" + 3 + else + 1 + end +end + +private def rand_quest(tier : Int32) + pool = config_get("quests.tier_#{tier}").as?(Array) + if !pool + return "" + end + + roll = rand(pool.size) + quest = pool[roll]?.as?(Hash) + + if !quest + return "" + end + + [ + roll, + type_to_i(quest["required_type"]?.as?(String)), + quest["required_amt"]? || 0, + quest["reward_diamonds"]? || 0, + quest["name"]? || "" + ].join(",") +end + +CrystalGauntlet.endpoints["/getGJChallenges.php"] = ->(context : HTTP::Server::Context): String { + params = URI::Params.parse(context.request.body.not_nil!.gets_to_end) + LOG.debug { params.inspect } + + if !config_get("quests.enabled").as?(Bool) + LOG.debug { "quests disabled" } + return "-1" + end + + user_id, account_id = Accounts.auth(params) + if !(user_id && account_id) + return "-1" + end + + time_left = get_quest_time(account_id) + + resp = [ + PAD_STR, + user_id, + String.new(get_chk_value(params["chk"])), + params["udid"], + account_id, + time_left, + + rand_quest(1), + rand_quest(2), + rand_quest(3) + ].join(":") + + resp_str = Base64.urlsafe_encode(XorCrypt.encrypt_string(resp, XorCrypt::QUESTS_XOR_KEY)) + + return PAD_STR + resp_str + "|" + Hashes.gen_solo_3(resp_str) +} diff --git a/src/endpoints/misc/getRewards.cr b/src/endpoints/misc/getRewards.cr index 0a0e8e5..22a5ced 100644 --- a/src/endpoints/misc/getRewards.cr +++ b/src/endpoints/misc/getRewards.cr @@ -2,14 +2,13 @@ require "uri" include CrystalGauntlet -XOR_KEY = "59182" -PAD_STR = "_____" # meaningless but necessary +private PAD_STR = "_____" # meaningless but necessary -def get_chk_value(chk_str : String) - XorCrypt.encrypt_string(Base64.decode_string(chk_str[5..]), XOR_KEY) +private def get_chk_value(chk_str : String) + XorCrypt.encrypt_string(Base64.decode_string(chk_str[5..]), XorCrypt::CHEST_XOR_KEY) end -def get_rand(type : String, large = false) +private def get_rand(type : String, large = false) base = "chests.#{large ? "large" : "small"}.#{type}" min = config_get("#{base}_min").as?(Int64) || 0 max = config_get("#{base}_max").as?(Int64) || 0 @@ -18,9 +17,9 @@ def get_rand(type : String, large = false) ((Random.rand(min.to_f .. (max.to_f + 1)) / increment).floor() * increment).to_i end -REWARD_TYPES = ["orbs", "diamonds", "shards", "keys"] +private REWARD_TYPES = ["orbs", "diamonds", "shards", "keys"] -def get_chest(account_id : Int32, large = false) : {Int32?, Int32?} +private def get_chest(account_id : Int32, large = false) : {Int32?, Int32?} begin total, next_at = DATABASE.query_one("select total_opened, next_at from #{large ? "large_chests" : "small_chests"} where account_id = ?", account_id, as: {Int32, String}) rescue @@ -30,7 +29,7 @@ def get_chest(account_id : Int32, large = false) : {Int32?, Int32?} end end -def claim_chest(account_id : Int32, prev_count : Int32, large = false) +private def claim_chest(account_id : Int32, prev_count : Int32, large = false) table = large ? "large_chests" : "small_chests" timer = config_get("chests.#{large ? "large" : "small"}.timer").as?(Int64) || 0 next_at = (Time.utc + timer.seconds).to_s(Format::TIME_FORMAT) @@ -92,7 +91,7 @@ CrystalGauntlet.endpoints["/getGJRewards.php"] = ->(context : HTTP::Server::Cont params["rewardType"].to_i? || 0 ].join(":") - resp_str = Base64.urlsafe_encode(XorCrypt.encrypt_string(resp, XOR_KEY)) + resp_str = Base64.urlsafe_encode(XorCrypt.encrypt_string(resp, XorCrypt::CHEST_XOR_KEY)) return PAD_STR + resp_str + "|" + Hashes.gen_solo_4(resp_str) } diff --git a/src/lib/format.cr b/src/lib/format.cr index 88f2854..284c443 100644 --- a/src/lib/format.cr +++ b/src/lib/format.cr @@ -91,23 +91,3 @@ module CrystalGauntlet::Format hash.map_with_index{ |(i, v)| "#{i}~#{fmt_value(v, false, false, true)}" }.join("~") end end - -module CrystalGauntlet::XorCrypt - extend self - - def encrypt(x : Bytes, key : Bytes) : Bytes - result = Bytes.new(x.size) - x.each.with_index() do |chr, index| - result[index] = (chr ^ key[index % key.size]) - end - result - end - - def encrypt_string(x : String, key : String) : Bytes - result = Bytes.new(x.bytesize) - x.bytes.each.with_index() do |chr, index| - result[index] = (chr ^ key.byte_at(index % key.bytesize)) - end - result - end -end diff --git a/src/lib/xor.cr b/src/lib/xor.cr new file mode 100644 index 0000000..8785164 --- /dev/null +++ b/src/lib/xor.cr @@ -0,0 +1,22 @@ +module CrystalGauntlet::XorCrypt + extend self + + def encrypt(x : Bytes, key : Bytes) : Bytes + result = Bytes.new(x.size) + x.each.with_index() do |chr, index| + result[index] = (chr ^ key[index % key.size]) + end + result + end + + def encrypt_string(x : String, key : String) : Bytes + result = Bytes.new(x.bytesize) + x.bytes.each.with_index() do |chr, index| + result[index] = (chr ^ key.byte_at(index % key.bytesize)) + end + result + end + + QUESTS_XOR_KEY = "19847" + CHEST_XOR_KEY = "59182" +end