| @@ -0,0 +1,217 @@ | |||
| 1 | + | <?php | |
| 2 | + | require_once 'engine/init.php'; | |
| 3 | + | ||
| 4 | + | if (lws_is_request()) { | |
| 5 | + | lws_handle(); | |
| 6 | + | } | |
| 7 | + | ||
| 8 | + | logged_in_redirect(); | |
| 9 | + | theme_open(); | |
| 10 | + | ||
| 11 | + | // --------------------------------------------------------------------------- | |
| 12 | + | // Step 2 of login: website 2FA v2 (independent of the game engine). | |
| 13 | + | // | |
| 14 | + | // Reached only after user_login() + the legacy TFS 2FA already succeeded, see | |
| 15 | + | // below. The pending account id lives in the session, never in the form, so a | |
| 16 | + | // visitor cannot skip straight here with an arbitrary account id. | |
| 17 | + | // --------------------------------------------------------------------------- | |
| 18 | + | if (isset($_POST['tfa2_code']) && isset($_SESSION['tfa2_pending']['id'])) { | |
| 19 | + | $pending = $_SESSION['tfa2_pending']; | |
| 20 | + | ||
| 21 | + | if (!Token::isValid($_POST['token'] ?? null)) { | |
| 22 | + | $errors[] = t('login.token_invalid'); | |
| 23 | + | } else if ((int)$pending['until'] < time()) { | |
| 24 | + | unset($_SESSION['tfa2_pending']); | |
| 25 | + | $errors[] = t_default('twofa2.expired', 'That verification step expired. Please log in again.'); | |
| 26 | + | } else { | |
| 27 | + | $accountId = (int)$pending['id']; | |
| 28 | + | $code = getValue($_POST['tfa2_code'] ?? null); | |
| 29 | + | ||
| 30 | + | if ($code !== false && znote2fa_verify_login_input($accountId, $code)) { | |
| 31 | + | unset($_SESSION['tfa2_pending']); | |
| 32 | + | setSession('user_id', $accountId); | |
| 33 | + | $_SESSION['tfa2_sv'] = znote2fa_session_version($accountId); | |
| 34 | + | Token::generate(); | |
| 35 | + | ||
| 36 | + | if (!empty($_POST['tfa2_trust']) && (int)znote2fa_v2_config()['trusted_device_days'] > 0) { | |
| 37 | + | znote2fa_trusted_device_issue($accountId); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | header('Location: myaccount.php'); | |
| 41 | + | exit(); | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | $errors[] = t_default('twofa2.wrong_code', 'That code is not valid. It may have expired, or you may have mistyped it.'); | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | if (isset($_SESSION['tfa2_pending']['id'])) { | |
| 49 | + | $pendingStatus = znote2fa_status((int)$_SESSION['tfa2_pending']['id']); | |
| 50 | + | ||
| 51 | + | if (empty($_POST['tfa2_email_sent']) && $pendingStatus['email_otp_enabled'] && !$pendingStatus['totp_enabled']) { | |
| 52 | + | $pendingUser = user_data((int)$_SESSION['tfa2_pending']['id'], 'email', 'name'); | |
| 53 | + | if (is_array($pendingUser) && !empty($pendingUser['email'])) { | |
| 54 | + | if (!znote2fa_email_send_code((int)$_SESSION['tfa2_pending']['id'], (string)$pendingUser['email'], (string)($pendingUser['name'] ?? ''))) { | |
| 55 | + | $errors[] = t_default('twofa2.email_delivery_failed', 'The verification e-mail could not be sent. Try again later or use a recovery code.'); | |
| 56 | + | } | |
| 57 | + | } else { | |
| 58 | + | $errors[] = t_default('twofa2.email_missing', 'This account has no valid e-mail address. Use a recovery code or contact an administrator.'); | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | view('login_2fa'); | |
| 62 | + | theme_close(); | |
| 63 | + | exit(); | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | if (empty($_POST) === false && !isset($_POST['tfa2_code'])) { | |
| 67 | + | ||
| 68 | + | if ($config['log_ip']) { | |
| 69 | + | znote_visitor_insert_detailed_data(5); | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | $username = $_POST['username']; | |
| 73 | + | $password = $_POST['password']; | |
| 74 | + | ||
| 75 | + | $loginGuardIp = znote_login_guard_ip(); | |
| 76 | + | $loginGuardLockedFor = znote_login_guard_lockout_remaining($loginGuardIp); | |
| 77 | + | ||
| 78 | + | if ($loginGuardLockedFor > 0) { | |
| 79 | + | $errors[] = t('login.too_many_attempts', ['minutes' => (int)ceil($loginGuardLockedFor / 60)]); | |
| 80 | + | } else if (empty($username) || empty($password)) { | |
| 81 | + | $errors[] = t('login.empty_fields'); | |
| 82 | + | } else if (strlen($username) > 32 || strlen($password) > 64) { | |
| 83 | + | $errors[] = t('login.too_long'); | |
| 84 | + | } else if (user_exist($username) === false) { | |
| 85 | + | znote_login_guard_record($loginGuardIp, (string)$username, false); | |
| 86 | + | $errors[] = t('login.not_found'); | |
| 87 | + | } /*else if (user_activated($username) === false) { | |
| 88 | + | $errors[] = t('login.not_activated'); | |
| 89 | + | } */else if (!Token::isValid($_POST['token'] ?? null)) { | |
| 90 | + | $errors[] = t('login.token_invalid'); | |
| 91 | + | } else { | |
| 92 | + | ||
| 93 | + | // Starting login. Delegated to the server adapter, which knows whether | |
| 94 | + | // this engine identifies an account by name or id and which password | |
| 95 | + | // scheme it uses (see engine/adapter/). | |
| 96 | + | $login = znote_server_adapter()->login($username, $password); | |
| 97 | + | if ($login === false) { | |
| 98 | + | znote_login_guard_record($loginGuardIp, (string)$username, false); | |
| 99 | + | $errors[] = t('login.wrong_combo'); | |
| 100 | + | } else { | |
| 101 | + | znote_login_guard_record($loginGuardIp, (string)$username, true); | |
| 102 | + | // Check if user have access to login | |
| 103 | + | $status = false; | |
| 104 | + | if ($config['mailserver']['register']) { | |
| 105 | + | $authenticate = db()->fetchOne( | |
| 106 | + | "SELECT `id` FROM `znote_accounts` WHERE `account_id` = ? AND `active` = 1 LIMIT 1;", | |
| 107 | + | [(int)$login] | |
| 108 | + | ); | |
| 109 | + | if ($authenticate !== false) { | |
| 110 | + | $status = true; | |
| 111 | + | } else { | |
| 112 | + | $errors[] = t('login.not_activated'); | |
| 113 | + | } | |
| 114 | + | } else $status = true; | |
| 115 | + | ||
| 116 | + | if ($status) { | |
| 117 | + | // Regular login success, now lets check authentication token code | |
| 118 | + | if (znote_server_adapter()->supportsLegacyTwoFactor() && $config['twoFactorAuthenticator']) { | |
| 119 | + | require_once("engine/function/rfc6238.php"); | |
| 120 | + | ||
| 121 | + | // Two factor authentication code / token | |
| 122 | + | $authcode = (isset($_POST['authcode'])) ? getValue($_POST['authcode'] ?? null) : false; | |
| 123 | + | ||
| 124 | + | // Load secret values from db | |
| 125 | + | $query = db()->fetchOne( | |
| 126 | + | "SELECT `a`.`secret` AS `secret`, `za`.`secret` AS `znote_secret` | |
| 127 | + | FROM `accounts` AS `a` | |
| 128 | + | INNER JOIN `znote_accounts` AS `za` ON `a`.`id` = `za`.`account_id` | |
| 129 | + | WHERE `a`.`id` = ? | |
| 130 | + | LIMIT 1;", | |
| 131 | + | [(int)$login] | |
| 132 | + | ); | |
| 133 | + | ||
| 134 | + | if ($query === false) { | |
| 135 | + | $errors[] = t('login.failed_title'); | |
| 136 | + | $status = false; | |
| 137 | + | ||
| 138 | + | // If account table HAS a secret, we need to validate it | |
| 139 | + | } else if ($query['secret'] !== NULL) { | |
| 140 | + | ||
| 141 | + | // Validate the secret first to make sure all is good. | |
| 142 | + | if (TokenAuth6238::verify($query['secret'], $authcode) !== true) { | |
| 143 | + | $errors[] = t('login.2fa_wrong'); | |
| 144 | + | $errors[] = t('login.2fa_hint'); | |
| 145 | + | $status = false; | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | } else { | |
| 149 | + | ||
| 150 | + | // secret from accounts table is null/not set. Perhaps we can activate it: | |
| 151 | + | if ($query['znote_secret'] !== NULL && $authcode !== false && !empty($authcode)) { | |
| 152 | + | ||
| 153 | + | // Validate the secret first to make sure all is good. | |
| 154 | + | if (TokenAuth6238::verify($query['znote_secret'], $authcode)) { | |
| 155 | + | // Success, enable the 2FA system | |
| 156 | + | db()->execute( | |
| 157 | + | "UPDATE `accounts` SET `secret` = ? WHERE `id` = ?;", | |
| 158 | + | [$query['znote_secret'], (int)$login] | |
| 159 | + | ); | |
| 160 | + | } else { | |
| 161 | + | $errors[] = t('login.2fa_activate_failed'); | |
| 162 | + | $errors[] = t('login.2fa_wrong'); | |
| 163 | + | $errors[] = t('login.2fa_hint'); | |
| 164 | + | $status = false; | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | } // End tfs 1.0+ with 2FA auth | |
| 169 | + | ||
| 170 | + | if ($status) { | |
| 171 | + | if (!znote_session_regenerate()) { | |
| 172 | + | $errors[] = t('login.failed_title'); | |
| 173 | + | $status = false; | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | if ($status) { | |
| 178 | + | $loginNameRow = user_data($login, 'id', 'name'); | |
| 179 | + | $isAdminAccount = has_admin_panel_access(is_array($loginNameRow) ? $loginNameRow : array()); | |
| 180 | + | ||
| 181 | + | if (znote2fa_required((int)$login, $isAdminAccount) && !znote2fa_trusted_device_check((int)$login)) { | |
| 182 | + | $_SESSION['tfa2_pending'] = array('id' => (int)$login, 'until' => time() + 300); | |
| 183 | + | header('Location: login.php'); | |
| 184 | + | exit(); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | setSession('user_id', $login); | |
| 188 | + | $_SESSION['tfa2_sv'] = znote2fa_session_version((int)$login); | |
| 189 | + | Token::generate(); | |
| 190 | + | ||
| 191 | + | // if IP is not set (etc acc created before Znote AAC was in use) | |
| 192 | + | $znote_data = user_znote_account_data($login, 'ip'); | |
| 193 | + | if ($znote_data['ip'] == 0) { | |
| 194 | + | $update_data = array( | |
| 195 | + | 'ip' => getIPLong(), | |
| 196 | + | ); | |
| 197 | + | user_update_znote_account($update_data); | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | // Send them to myaccount.php | |
| 201 | + | header('Location: myaccount.php'); | |
| 202 | + | exit(); | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | if (empty($errors) === false) { | |
| 210 | + | header("HTTP/1.1 401 Not Found"); | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | if (empty($_POST) === true || empty($errors) === false) { | |
| 214 | + | view('login_form'); | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | theme_close(); ?> |
| @@ -0,0 +1,6 @@ | |||
| 1 | + | <?php | |
| 2 | + | require_once 'engine/init.php'; | |
| 3 | + | ||
| 4 | + | znote_session_destroy(); | |
| 5 | + | header('Location: index.php'); | |
| 6 | + | exit; |
| @@ -0,0 +1,38 @@ | |||
| 1 | + | Znote AAC server scripts for Canary (opentibiabr) and otservbr-global. | |
| 2 | + | ||
| 3 | + | WHERE TO PUT THEM | |
| 4 | + | Canary: data/scripts/ (or data-otservbr-global/scripts/ on | |
| 5 | + | an otservbr-global data pack) | |
| 6 | + | otservbr-global: data-otservbr-global/scripts/ | |
| 7 | + | Drop the .lua files in there (any sub-folder is fine) and restart the server | |
| 8 | + | or run "/reload scripts". | |
| 9 | + | ||
| 10 | + | FILES | |
| 11 | + | znoteshop_talkaction.lua !shop - player pulls their website shop orders. | |
| 12 | + | znoteshop_globalevent.lua Auto-delivers the same orders every 30s to online | |
| 13 | + | buyers standing in a protection zone. Use BOTH, or | |
| 14 | + | just the talkaction for manual delivery only. | |
| 15 | + | znote_login.lua Syncs SERVER_NAME / IP / GAME_PORT from config.lua | |
| 16 | + | into znote_global_storage for the AAC login web | |
| 17 | + | service. Needs an up-to-date Znote AAC. | |
| 18 | + | powergamers.lua Daily exp / online-time history for powergamers.php. | |
| 19 | + | Run the ALTER TABLE at the top of the file once. | |
| 20 | + | player_history.lua Per-logout skill snapshots (auto-creates its table). | |
| 21 | + | sync_outfit.lua Marks which full outfits a character owns so | |
| 22 | + | characterprofile.php can show them. storage_value | |
| 23 | + | must match $config['EQ_shower'] in the AAC. The | |
| 24 | + | look type lists are the complete Female / Male sets | |
| 25 | + | from canary data/XML/outfits.xml (127 / 125). | |
| 26 | + | ||
| 27 | + | COMPATIBILITY NOTES | |
| 28 | + | - Every Canary / otservbr-global protocol (10.98+: otservbr-global 12.x, | |
| 29 | + | Canary 13.x, and the current 15.x) supports outfits, addons, mounts and | |
| 30 | + | houses, so the shop scripts handle order types 1, 5, 6 and 7 unconditionally | |
| 31 | + | - no Game.getClientVersion() guard. | |
| 32 | + | - Canary / otservbr-global already log deaths to `player_deaths` and hand out | |
| 33 | + | first items on their data pack, so the TFS playerdeath.lua / first_items.lua | |
| 34 | + | from Lua/TFS_10 are NOT included here - use the engine's own systems. | |
| 35 | + | - House order type 7 uses house:setHouseOwner(guid, true) (the Canary name). | |
| 36 | + | On plain TFS use Lua/TFS_10 instead - that build calls house:setOwnerGuid(). | |
| 37 | + | - Message constants used: MESSAGE_EVENT_ADVANCE (success), MESSAGE_FAILURE | |
| 38 | + | (blocked), MESSAGE_STATUS (info). Swap them if your build differs. |
| @@ -0,0 +1,229 @@ | |||
| 1 | + | -- Auto install tables if we dont got them yet (first install) | |
| 2 | + | db.query([[ | |
| 3 | + | CREATE TABLE IF NOT EXISTS `player_history_skill` ( | |
| 4 | + | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, | |
| 5 | + | `player_id` int(11) NOT NULL, | |
| 6 | + | `lastlogin` bigint(20) unsigned NOT NULL, | |
| 7 | + | `lastlogout` bigint(20) unsigned NOT NULL, | |
| 8 | + | `town_id` int(11) NOT NULL, | |
| 9 | + | `lastip` int(10) unsigned NOT NULL, | |
| 10 | + | `skull` tinyint(1) NOT NULL, | |
| 11 | + | `blessings` tinyint(2) NOT NULL, | |
| 12 | + | `onlinetime` int(11) NOT NULL, | |
| 13 | + | `balance` bigint(20) unsigned NOT NULL, | |
| 14 | + | `level` int(11) NOT NULL, | |
| 15 | + | `experience` bigint(20) NOT NULL, | |
| 16 | + | `maglevel` int(11) NOT NULL, | |
| 17 | + | `skill_fist` int(10) unsigned NOT NULL, | |
| 18 | + | `skill_club` int(10) unsigned NOT NULL, | |
| 19 | + | `skill_sword` int(10) unsigned NOT NULL, | |
| 20 | + | `skill_axe` int(10) unsigned NOT NULL, | |
| 21 | + | `skill_dist` int(10) unsigned NOT NULL, | |
| 22 | + | `skill_shielding` int(10) unsigned NOT NULL, | |
| 23 | + | `skill_fishing` int(10) unsigned NOT NULL, | |
| 24 | + | PRIMARY KEY (`id`), | |
| 25 | + | FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE | |
| 26 | + | ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; | |
| 27 | + | ]]) | |
| 28 | + | ||
| 29 | + | ||
| 30 | + | -- Auto populate table if it is empty | |
| 31 | + | local resultId = db.storeQuery("SELECT `id` FROM `player_history_skill` LIMIT 1;") | |
| 32 | + | if resultId == false then | |
| 33 | + | db.asyncQuery([[ | |
| 34 | + | INSERT INTO `player_history_skill` ( | |
| 35 | + | `player_id`, | |
| 36 | + | `lastlogin`, | |
| 37 | + | `lastlogout`, | |
| 38 | + | `town_id`, | |
| 39 | + | `lastip`, | |
| 40 | + | `skull`, | |
| 41 | + | `blessings`, | |
| 42 | + | `onlinetime`, | |
| 43 | + | `balance`, | |
| 44 | + | `level`, | |
| 45 | + | `experience`, | |
| 46 | + | `maglevel`, | |
| 47 | + | `skill_fist`, | |
| 48 | + | `skill_club`, | |
| 49 | + | `skill_sword`, | |
| 50 | + | `skill_axe`, | |
| 51 | + | `skill_dist`, | |
| 52 | + | `skill_shielding`, | |
| 53 | + | `skill_fishing` | |
| 54 | + | ) | |
| 55 | + | SELECT | |
| 56 | + | `p`.`id` AS `player_id`, | |
| 57 | + | `zp`.`created` AS `lastlogin`, | |
| 58 | + | CASE WHEN `p`.`lastlogout` > 0 | |
| 59 | + | THEN `p`.`lastlogout` | |
| 60 | + | ELSE `zp`.`created` | |
| 61 | + | END AS `lastlogout`, | |
| 62 | + | `p`.`town_id`, | |
| 63 | + | `p`.`lastip`, | |
| 64 | + | `p`.`skull`, | |
| 65 | + | `p`.`blessings`, | |
| 66 | + | `p`.`onlinetime`, | |
| 67 | + | `p`.`balance`, | |
| 68 | + | `p`.`level`, | |
| 69 | + | `p`.`experience`, | |
| 70 | + | `p`.`maglevel`, | |
| 71 | + | `p`.`skill_fist`, | |
| 72 | + | `p`.`skill_club`, | |
| 73 | + | `p`.`skill_sword`, | |
| 74 | + | `p`.`skill_axe`, | |
| 75 | + | `p`.`skill_dist`, | |
| 76 | + | `p`.`skill_shielding`, | |
| 77 | + | `p`.`skill_fishing` | |
| 78 | + | FROM `players` AS `p` | |
| 79 | + | INNER JOIN `znote_players` AS `zp` | |
| 80 | + | ON `p`.`id` = `zp`.`player_id` | |
| 81 | + | ORDER BY `zp`.`created` | |
| 82 | + | ]]) | |
| 83 | + | else | |
| 84 | + | result.free(resultId) | |
| 85 | + | end | |
| 86 | + | ||
| 87 | + | ||
| 88 | + | -- Logout event, triggered by logout, and death | |
| 89 | + | function historyLogoutEvent(player) | |
| 90 | + | local blessdec = 0 | |
| 91 | + | local i = 0 | |
| 92 | + | while player:hasBlessing(i+1) do | |
| 93 | + | blessdec = blessdec+2^i | |
| 94 | + | i = i+1 | |
| 95 | + | end | |
| 96 | + | ||
| 97 | + | local playerGuid = player:getGuid() | |
| 98 | + | db.query([[ | |
| 99 | + | INSERT INTO `player_history_skill` ( | |
| 100 | + | `player_id`, | |
| 101 | + | `lastlogin`, | |
| 102 | + | `lastlogout`, | |
| 103 | + | `town_id`, | |
| 104 | + | `lastip`, | |
| 105 | + | `skull`, | |
| 106 | + | `blessings`, | |
| 107 | + | `onlinetime`, | |
| 108 | + | `balance`, | |
| 109 | + | `level`, | |
| 110 | + | `experience`, | |
| 111 | + | `maglevel`, | |
| 112 | + | `skill_fist`, | |
| 113 | + | `skill_club`, | |
| 114 | + | `skill_sword`, | |
| 115 | + | `skill_axe`, | |
| 116 | + | `skill_dist`, | |
| 117 | + | `skill_shielding`, | |
| 118 | + | `skill_fishing` | |
| 119 | + | ) VALUES ( | |
| 120 | + | ]]..table.concat({ | |
| 121 | + | playerGuid, | |
| 122 | + | player:getLastLoginSaved(), | |
| 123 | + | os.time(), | |
| 124 | + | player:getTown():getId(), | |
| 125 | + | player:getIp(), | |
| 126 | + | player:getSkull(), | |
| 127 | + | blessdec, | |
| 128 | + | "(SELECT `onlinetime` FROM `players` WHERE `id`='"..playerGuid.."') + ".. os.time() - player:getLastLoginSaved(), | |
| 129 | + | player:getBankBalance(), | |
| 130 | + | player:getLevel(), | |
| 131 | + | player:getExperience(), | |
| 132 | + | player:getMagicLevel(), | |
| 133 | + | player:getSkillLevel(SKILL_FIST), | |
| 134 | + | player:getSkillLevel(SKILL_CLUB), | |
| 135 | + | player:getSkillLevel(SKILL_SWORD), | |
| 136 | + | player:getSkillLevel(SKILL_AXE), | |
| 137 | + | player:getSkillLevel(SKILL_DISTANCE), | |
| 138 | + | player:getSkillLevel(SKILL_SHIELD), | |
| 139 | + | player:getSkillLevel(SKILL_FISHING) | |
| 140 | + | }, ",")..[[ | |
| 141 | + | ); | |
| 142 | + | ]]) | |
| 143 | + | end | |
| 144 | + | ||
| 145 | + | ||
| 146 | + | -- Log player state on logout | |
| 147 | + | local player_history_skill = CreatureEvent("player_history_skill") | |
| 148 | + | function player_history_skill.onLogout(player) | |
| 149 | + | --print("2-logout["..player:getName().."]") | |
| 150 | + | historyLogoutEvent(player) | |
| 151 | + | return true | |
| 152 | + | end | |
| 153 | + | player_history_skill:register() | |
| 154 | + | ||
| 155 | + | ||
| 156 | + | -- And on death | |
| 157 | + | local player_history_skill_death = CreatureEvent("player_history_skill_death") | |
| 158 | + | function player_history_skill_death.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) | |
| 159 | + | --print("3-death["..creature:getName().."]") | |
| 160 | + | historyLogoutEvent(Player(creature)) | |
| 161 | + | end | |
| 162 | + | player_history_skill_death:register() | |
| 163 | + | ||
| 164 | + | ||
| 165 | + | -- If this is first login, insert current progress | |
| 166 | + | local player_history_skill_login = CreatureEvent("player_history_skill_login") | |
| 167 | + | function player_history_skill_login.onLogin(player) | |
| 168 | + | --print("1-login["..player:getName().."]") | |
| 169 | + | player:registerEvent("player_history_skill_death") | |
| 170 | + | ||
| 171 | + | local playerGuid = player:getGuid() | |
| 172 | + | local resultId = db.storeQuery("SELECT `id` FROM `player_history_skill` WHERE `player_id`="..playerGuid.." LIMIT 1;") | |
| 173 | + | if resultId == false then | |
| 174 | + | db.query([[ | |
| 175 | + | INSERT INTO `player_history_skill` ( | |
| 176 | + | `player_id`, | |
| 177 | + | `lastlogin`, | |
| 178 | + | `lastlogout`, | |
| 179 | + | `town_id`, | |
| 180 | + | `lastip`, | |
| 181 | + | `skull`, | |
| 182 | + | `blessings`, | |
| 183 | + | `onlinetime`, | |
| 184 | + | `balance`, | |
| 185 | + | `level`, | |
| 186 | + | `experience`, | |
| 187 | + | `maglevel`, | |
| 188 | + | `skill_fist`, | |
| 189 | + | `skill_club`, | |
| 190 | + | `skill_sword`, | |
| 191 | + | `skill_axe`, | |
| 192 | + | `skill_dist`, | |
| 193 | + | `skill_shielding`, | |
| 194 | + | `skill_fishing` | |
| 195 | + | ) | |
| 196 | + | SELECT | |
| 197 | + | `p`.`id` AS `player_id`, | |
| 198 | + | `zp`.`created` AS `lastlogin`, | |
| 199 | + | CASE WHEN `p`.`lastlogout` > 0 | |
| 200 | + | THEN `p`.`lastlogout` | |
| 201 | + | ELSE `zp`.`created` | |
| 202 | + | END AS `lastlogout`, | |
| 203 | + | `p`.`town_id`, | |
| 204 | + | `p`.`lastip`, | |
| 205 | + | `p`.`skull`, | |
| 206 | + | `p`.`blessings`, | |
| 207 | + | `p`.`onlinetime`, | |
| 208 | + | `p`.`balance`, | |
| 209 | + | `p`.`level`, | |
| 210 | + | `p`.`experience`, | |
| 211 | + | `p`.`maglevel`, | |
| 212 | + | `p`.`skill_fist`, | |
| 213 | + | `p`.`skill_club`, | |
| 214 | + | `p`.`skill_sword`, | |
| 215 | + | `p`.`skill_axe`, | |
| 216 | + | `p`.`skill_dist`, | |
| 217 | + | `p`.`skill_shielding`, | |
| 218 | + | `p`.`skill_fishing` | |
| 219 | + | FROM `players` AS `p` | |
| 220 | + | INNER JOIN `znote_players` AS `zp` | |
| 221 | + | ON `p`.`id` = `zp`.`player_id` | |
| 222 | + | WHERE `p`.`id` = ]]..playerGuid..[[ | |
| 223 | + | ]]) | |
| 224 | + | else | |
| 225 | + | result.free(resultId) | |
| 226 | + | end | |
| 227 | + | return true | |
| 228 | + | end | |
| 229 | + | player_history_skill_login:register() |
| @@ -0,0 +1,65 @@ | |||
| 1 | + | -- getEternalStorage and setEternalStorage | |
| 2 | + | -- can be added to data/global.lua if you want to use eternal storage for another purpose than this. | |
| 3 | + | -- Regular TFS global storage values get reset every time server reboots. This does not. | |
| 4 | + | local function getEternalStorage(key, parser) | |
| 5 | + | local value = result.getString(db.storeQuery("SELECT `value` FROM `znote_global_storage` WHERE `key` = ".. key .. ";"), "value") | |
| 6 | + | if not value then | |
| 7 | + | if parser then | |
| 8 | + | return false | |
| 9 | + | else | |
| 10 | + | return -1 | |
| 11 | + | end | |
| 12 | + | end | |
| 13 | + | result.free(value) | |
| 14 | + | return tonumber(value) or value | |
| 15 | + | end | |
| 16 | + | ||
| 17 | + | local function setEternalStorage(key, value) | |
| 18 | + | if getEternalStorage(key, true) then | |
| 19 | + | db.query("UPDATE `znote_global_storage` SET `value` = '".. value .. "' WHERE `key` = ".. key .. ";") | |
| 20 | + | else | |
| 21 | + | db.query("INSERT INTO `znote_global_storage` (`key`, `value`) VALUES (".. key ..", ".. value ..");") | |
| 22 | + | end | |
| 23 | + | return true | |
| 24 | + | end | |
| 25 | + | ||
| 26 | + | -- SQL Query to execute: -- | |
| 27 | + | --[[ | |
| 28 | + | ALTER TABLE `znote_players` ADD `exphist_lastexp` BIGINT NOT NULL DEFAULT '0', | |
| 29 | + | ADD `exphist1` BIGINT NOT NULL DEFAULT '0', | |
| 30 | + | ADD `exphist2` BIGINT NOT NULL DEFAULT '0', | |
| 31 | + | ADD `exphist3` BIGINT NOT NULL DEFAULT '0', | |
| 32 | + | ADD `exphist4` BIGINT NOT NULL DEFAULT '0', | |
| 33 | + | ADD `exphist5` BIGINT NOT NULL DEFAULT '0', | |
| 34 | + | ADD `exphist6` BIGINT NOT NULL DEFAULT '0', | |
| 35 | + | ADD `exphist7` BIGINT NOT NULL DEFAULT '0', | |
| 36 | + | ADD `onlinetimetoday` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 37 | + | ADD `onlinetime1` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 38 | + | ADD `onlinetime2` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 39 | + | ADD `onlinetime3` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 40 | + | ADD `onlinetime4` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 41 | + | ADD `onlinetime5` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 42 | + | ADD `onlinetime6` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 43 | + | ADD `onlinetime7` MEDIUMINT UNSIGNED NOT NULL DEFAULT '0', | |
| 44 | + | ADD `onlinetimeall` INT UNSIGNED NOT NULL DEFAULT '0'; | |
| 45 | + | ]]-- | |
| 46 | + | ||
| 47 | + | -- after that execute: -- | |
| 48 | + | --[[ | |
| 49 | + | UPDATE `znote_players` AS `z` INNER JOIN `players` AS `p` ON `p`.`id`=`z`.`player_id` SET `z`.`exphist_lastexp`=`p`.`experience`; | |
| 50 | + | ]]-- | |
| 51 | + | ||
| 52 | + | local globalevent = GlobalEvent("PowerGamers") | |
| 53 | + | ||
| 54 | + | function globalevent.onThink(...) | |
| 55 | + | if tonumber(os.date("%d")) ~= getEternalStorage(23856) then | |
| 56 | + | setEternalStorage(23856, (tonumber(os.date("%d")))) | |
| 57 | + | db.query("UPDATE `znote_players` SET `onlinetime7`=`onlinetime6`, `onlinetime6`=`onlinetime5`, `onlinetime5`=`onlinetime4`, `onlinetime4`=`onlinetime3`, `onlinetime3`=`onlinetime2`, `onlinetime2`=`onlinetime1`, `onlinetime1`=`onlinetimetoday`, `onlinetimetoday`=0;") | |
| 58 | + | db.query("UPDATE `znote_players` `z` INNER JOIN `players` `p` ON `p`.`id`=`z`.`player_id` SET `z`.`exphist7`=`z`.`exphist6`, `z`.`exphist6`=`z`.`exphist5`, `z`.`exphist5`=`z`.`exphist4`, `z`.`exphist4`=`z`.`exphist3`, `z`.`exphist3`=`z`.`exphist2`, `z`.`exphist2`=`z`.`exphist1`, `z`.`exphist1`=`p`.`experience`-`z`.`exphist_lastexp`, `z`.`exphist_lastexp`=`p`.`experience`;") | |
| 59 | + | end | |
| 60 | + | db.query("UPDATE `znote_players` SET `onlinetimetoday` = `onlinetimetoday` + 60, `onlinetimeall` = `onlinetimeall` + 60 WHERE `player_id` IN (SELECT `player_id` FROM `players_online` WHERE `players_online`.`player_id` = `znote_players`.`player_id`)") | |
| 61 | + | return true | |
| 62 | + | end | |
| 63 | + | ||
| 64 | + | globalevent:interval(60000) | |
| 65 | + | globalevent:register() |
| @@ -0,0 +1,57 @@ | |||
| 1 | + | -- Sync the full outfits (all 3 addons) a character owns into Znote AAC storage, | |
| 2 | + | -- so characterprofile.php can show them. Canary / otservbr-global. | |
| 3 | + | -- | |
| 4 | + | -- looktype lists below are the complete Female / Male outfit sets from | |
| 5 | + | -- opentibiabr/canary data/XML/outfits.xml. Regenerate them from that file if | |
| 6 | + | -- you add custom outfits. | |
| 7 | + | ||
| 8 | + | znote_outfit_list = { | |
| 9 | + | { -- Female outfits (127) | |
| 10 | + | 136, 137, 138, 139, 140, 141, 142, 147, 148, 149, | |
| 11 | + | 150, 155, 156, 157, 158, 252, 269, 270, 279, 288, | |
| 12 | + | 324, 329, 336, 366, 431, 433, 464, 466, 471, 513, | |
| 13 | + | 514, 542, 575, 578, 618, 620, 632, 635, 636, 664, | |
| 14 | + | 666, 683, 694, 696, 698, 724, 732, 745, 749, 759, | |
| 15 | + | 845, 852, 874, 885, 900, 909, 929, 956, 958, 963, | |
| 16 | + | 965, 967, 969, 971, 973, 975, 1020, 1024, 1043, 1050, | |
| 17 | + | 1057, 1070, 1095, 1103, 1128, 1147, 1162, 1174, 1187, 1203, | |
| 18 | + | 1205, 1207, 1211, 1244, 1246, 1252, 1271, 1280, 1283, 1289, | |
| 19 | + | 1293, 1323, 1332, 1339, 1372, 1383, 1385, 1387, 1416, 1437, | |
| 20 | + | 1445, 1450, 1456, 1461, 1490, 1501, 1569, 1576, 1582, 1598, | |
| 21 | + | 1613, 1619, 1663, 1676, 1681, 1714, 1723, 1726, 1746, 1775, | |
| 22 | + | 1777, 1808, 1825, 1832, 1838, 1860, 1861 | |
| 23 | + | }, | |
| 24 | + | { -- Male outfits (125) | |
| 25 | + | 128, 129, 130, 131, 132, 133, 134, 143, 144, 145, | |
| 26 | + | 146, 151, 152, 153, 154, 251, 268, 273, 278, 289, | |
| 27 | + | 325, 328, 335, 367, 430, 432, 463, 465, 472, 512, | |
| 28 | + | 516, 541, 574, 577, 610, 619, 633, 634, 637, 665, | |
| 29 | + | 667, 684, 695, 697, 699, 725, 733, 746, 750, 760, | |
| 30 | + | 846, 853, 873, 884, 899, 908, 931, 955, 957, 962, | |
| 31 | + | 964, 966, 968, 970, 972, 974, 1021, 1023, 1042, 1051, | |
| 32 | + | 1056, 1069, 1094, 1102, 1127, 1146, 1161, 1173, 1186, 1202, | |
| 33 | + | 1204, 1206, 1210, 1243, 1245, 1251, 1270, 1279, 1282, 1288, | |
| 34 | + | 1292, 1322, 1331, 1338, 1371, 1382, 1384, 1386, 1415, 1436, | |
| 35 | + | 1444, 1449, 1457, 1460, 1489, 1500, 1568, 1575, 1581, 1597, | |
| 36 | + | 1612, 1618, 1662, 1675, 1680, 1713, 1722, 1725, 1745, 1774, | |
| 37 | + | 1776, 1809, 1824, 1831, 1837 | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | local syncOutfit = CreatureEvent("ZnoteSyncOutfit") | |
| 42 | + | ||
| 43 | + | function syncOutfit.onLogin(player) | |
| 44 | + | -- storage_value .. storage_value + highest look type must be free. | |
| 45 | + | -- Must match $config['EQ_shower'] in the Znote AAC config.php. | |
| 46 | + | local storage_value = 10000 | |
| 47 | + | for _, lookType in ipairs(znote_outfit_list[player:getSex() + 1]) do | |
| 48 | + | if player:hasOutfit(lookType, 3) then | |
| 49 | + | if player:getStorageValue(storage_value + lookType) ~= 3 then | |
| 50 | + | player:setStorageValue(storage_value + lookType, 3) | |
| 51 | + | end | |
| 52 | + | end | |
| 53 | + | end | |
| 54 | + | return true | |
| 55 | + | end | |
| 56 | + | ||
| 57 | + | syncOutfit:register() |
| @@ -0,0 +1,104 @@ | |||
| 1 | + | -- Znote Shop auto-delivery for Canary (opentibiabr) / otservbr-global. | |
| 2 | + | -- Every 30 seconds it hands pending item / outfit / mount / house orders to the | |
| 3 | + | -- buyer if they are online AND standing in a protection zone. Players can still | |
| 4 | + | -- force delivery with !shop (see znoteshop_talkaction.lua) - keep both. | |
| 5 | + | ||
| 6 | + | local znoteShopGlobal = GlobalEvent("ZnoteShopDelivery") | |
| 7 | + | ||
| 8 | + | function znoteShopGlobal.onThink(interval) | |
| 9 | + | local orders = db.storeQuery([[ | |
| 10 | + | SELECT | |
| 11 | + | MIN(`po`.`player_id`) AS `player_id`, | |
| 12 | + | `shop`.`id`, | |
| 13 | + | `shop`.`type`, | |
| 14 | + | `shop`.`itemid`, | |
| 15 | + | `shop`.`count` | |
| 16 | + | FROM `players_online` AS `po` | |
| 17 | + | INNER JOIN `players` AS `p` ON `po`.`player_id` = `p`.`id` | |
| 18 | + | INNER JOIN `znote_shop_orders` AS `shop` ON `p`.`account_id` = `shop`.`account_id` | |
| 19 | + | WHERE `shop`.`type` IN (1, 5, 6, 7) | |
| 20 | + | GROUP BY `shop`.`id` | |
| 21 | + | ]]) | |
| 22 | + | if orders == false then | |
| 23 | + | return true | |
| 24 | + | end | |
| 25 | + | ||
| 26 | + | repeat | |
| 27 | + | local playerId = result.getNumber(orders, "player_id") | |
| 28 | + | local oId = result.getNumber(orders, "id") | |
| 29 | + | local oType = result.getNumber(orders, "type") | |
| 30 | + | local oItemId = result.getNumber(orders, "itemid") | |
| 31 | + | local oCount = result.getNumber(orders, "count") | |
| 32 | + | ||
| 33 | + | local player = Player(playerId) | |
| 34 | + | if player then | |
| 35 | + | local tile = Tile(player:getPosition()) | |
| 36 | + | if tile and tile:hasFlag(TILESTATE_PROTECTIONZONE) then | |
| 37 | + | ||
| 38 | + | if oType == 1 then | |
| 39 | + | local itemType = ItemType(oItemId) | |
| 40 | + | local needSlots = itemType:isStackable() and (math.floor(oCount / 100) + 1) or oCount | |
| 41 | + | if player:getFreeCapacity() < itemType:getWeight(oCount) then | |
| 42 | + | player:sendTextMessage(MESSAGE_FAILURE, "You have a pending shop order but not enough capacity.") | |
| 43 | + | elseif player:getFreeBackpackSlots() < needSlots then | |
| 44 | + | player:sendTextMessage(MESSAGE_FAILURE, ("You have a pending shop order - free %d backpack slots."):format(needSlots)) | |
| 45 | + | else | |
| 46 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 47 | + | player:addItem(oItemId, oCount) | |
| 48 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, ("You received %dx %s."):format(oCount, itemType:getName())) | |
| 49 | + | end | |
| 50 | + | ||
| 51 | + | elseif oType == 5 then | |
| 52 | + | local looks = {} | |
| 53 | + | local packed = oItemId | |
| 54 | + | if packed > 1000 then | |
| 55 | + | local male = math.floor(packed / 1000) | |
| 56 | + | table.insert(looks, male) | |
| 57 | + | packed = packed - (male * 1000) | |
| 58 | + | end | |
| 59 | + | table.insert(looks, packed) | |
| 60 | + | for _, lookType in ipairs(looks) do | |
| 61 | + | if not player:hasOutfit(lookType, oCount) then | |
| 62 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 63 | + | player:addOutfit(lookType) | |
| 64 | + | player:addOutfitAddon(lookType, oCount) | |
| 65 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You received a new outfit.") | |
| 66 | + | else | |
| 67 | + | player:sendTextMessage(MESSAGE_FAILURE, "You already own a purchased outfit and addon.") | |
| 68 | + | end | |
| 69 | + | end | |
| 70 | + | ||
| 71 | + | elseif oType == 6 then | |
| 72 | + | if not player:hasMount(oItemId) then | |
| 73 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 74 | + | player:addMount(oItemId) | |
| 75 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You received a new mount.") | |
| 76 | + | else | |
| 77 | + | player:sendTextMessage(MESSAGE_FAILURE, "You already own a purchased mount.") | |
| 78 | + | end | |
| 79 | + | ||
| 80 | + | elseif oType == 7 then | |
| 81 | + | local house = House(oItemId) | |
| 82 | + | local buyer = db.storeQuery("SELECT `name` FROM `players` WHERE `id` = " .. oCount .. " LIMIT 1;") | |
| 83 | + | if buyer ~= false then | |
| 84 | + | local buyerName = result.getString(buyer, "name") | |
| 85 | + | result.free(buyer) | |
| 86 | + | if house then | |
| 87 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 88 | + | house:setHouseOwner(oCount, true) | |
| 89 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, ("House %s now belongs to %s."):format(house:getName(), buyerName)) | |
| 90 | + | end | |
| 91 | + | end | |
| 92 | + | end | |
| 93 | + | ||
| 94 | + | else | |
| 95 | + | player:sendTextMessage(MESSAGE_STATUS, "You have a pending shop order - stand in a protection zone to receive it.") | |
| 96 | + | end | |
| 97 | + | end | |
| 98 | + | until not result.next(orders) | |
| 99 | + | result.free(orders) | |
| 100 | + | return true | |
| 101 | + | end | |
| 102 | + | ||
| 103 | + | znoteShopGlobal:interval(30000) | |
| 104 | + | znoteShopGlobal:register() |
| @@ -0,0 +1,107 @@ | |||
| 1 | + | -- Znote Shop for Znote AAC on Canary (opentibiabr) / otservbr-global. | |
| 2 | + | -- Players type !shop while standing in a protection zone to receive the orders | |
| 3 | + | -- they bought on the website. | |
| 4 | + | -- | |
| 5 | + | -- Order types (znote_shop_orders.type): | |
| 6 | + | -- 1 item id itemid = item id, count = amount | |
| 7 | + | -- 2 premium handled by the website | |
| 8 | + | -- 3 gender change handled by the website | |
| 9 | + | -- 4 name change handled by the website | |
| 10 | + | -- 5 outfit + addons itemid = male*1000 + female (or a single look type), | |
| 11 | + | -- count = addon bitmask (1, 2 or 3) | |
| 12 | + | -- 6 mount itemid = mount id | |
| 13 | + | -- 7 instant house itemid = house id, count = buyer player id | |
| 14 | + | ||
| 15 | + | local znoteShopTalk = TalkAction("!shop") | |
| 16 | + | ||
| 17 | + | function znoteShopTalk.onSay(player, words, param) | |
| 18 | + | local storage = 54073 -- unused storage, throttles !shop against SQL spam | |
| 19 | + | local cooldown = 15 -- seconds | |
| 20 | + | ||
| 21 | + | local remaining = player:getStorageValue(storage) - os.time() | |
| 22 | + | if remaining > 0 then | |
| 23 | + | player:sendTextMessage(MESSAGE_STATUS, ("You can use !shop again in %d seconds."):format(remaining)) | |
| 24 | + | return true | |
| 25 | + | end | |
| 26 | + | player:setStorageValue(storage, os.time() + cooldown) | |
| 27 | + | ||
| 28 | + | local orders = db.storeQuery("SELECT `id`, `type`, `itemid`, `count` FROM `znote_shop_orders` WHERE `account_id` = " .. player:getAccountId() .. ";") | |
| 29 | + | if orders == false then | |
| 30 | + | player:sendTextMessage(MESSAGE_STATUS, "You have no orders.") | |
| 31 | + | return true | |
| 32 | + | end | |
| 33 | + | ||
| 34 | + | local served = false | |
| 35 | + | repeat | |
| 36 | + | local oId = result.getNumber(orders, "id") | |
| 37 | + | local oType = result.getNumber(orders, "type") | |
| 38 | + | local oItemId = result.getNumber(orders, "itemid") | |
| 39 | + | local oCount = result.getNumber(orders, "count") | |
| 40 | + | ||
| 41 | + | if oType == 1 then | |
| 42 | + | served = true | |
| 43 | + | local itemType = ItemType(oItemId) | |
| 44 | + | if player:getFreeCapacity() >= itemType:getWeight(oCount) then | |
| 45 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 46 | + | player:addItem(oItemId, oCount) | |
| 47 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, ("You received %dx %s."):format(oCount, itemType:getName())) | |
| 48 | + | else | |
| 49 | + | player:sendTextMessage(MESSAGE_FAILURE, "You need more capacity to carry this order.") | |
| 50 | + | end | |
| 51 | + | ||
| 52 | + | elseif oType == 5 then | |
| 53 | + | served = true | |
| 54 | + | local looks = {} | |
| 55 | + | local packed = oItemId | |
| 56 | + | if packed > 1000 then | |
| 57 | + | local male = math.floor(packed / 1000) | |
| 58 | + | table.insert(looks, male) | |
| 59 | + | packed = packed - (male * 1000) | |
| 60 | + | end | |
| 61 | + | table.insert(looks, packed) | |
| 62 | + | for _, lookType in ipairs(looks) do | |
| 63 | + | if not player:hasOutfit(lookType, oCount) then | |
| 64 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 65 | + | player:addOutfit(lookType) | |
| 66 | + | player:addOutfitAddon(lookType, oCount) | |
| 67 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You received a new outfit.") | |
| 68 | + | else | |
| 69 | + | player:sendTextMessage(MESSAGE_FAILURE, "You already own this outfit and addon.") | |
| 70 | + | end | |
| 71 | + | end | |
| 72 | + | ||
| 73 | + | elseif oType == 6 then | |
| 74 | + | served = true | |
| 75 | + | if not player:hasMount(oItemId) then | |
| 76 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 77 | + | player:addMount(oItemId) | |
| 78 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You received a new mount.") | |
| 79 | + | else | |
| 80 | + | player:sendTextMessage(MESSAGE_FAILURE, "You already own this mount.") | |
| 81 | + | end | |
| 82 | + | ||
| 83 | + | elseif oType == 7 then | |
| 84 | + | served = true | |
| 85 | + | local house = House(oItemId) | |
| 86 | + | local buyer = db.storeQuery("SELECT `name` FROM `players` WHERE `id` = " .. oCount .. " LIMIT 1;") | |
| 87 | + | if buyer ~= false then | |
| 88 | + | local buyerName = result.getString(buyer, "name") | |
| 89 | + | result.free(buyer) | |
| 90 | + | if house then | |
| 91 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. oId .. ";") | |
| 92 | + | house:setHouseOwner(oCount, true) | |
| 93 | + | player:sendTextMessage(MESSAGE_EVENT_ADVANCE, ("House %s now belongs to %s. Keep the rent in the bank."):format(house:getName(), buyerName)) | |
| 94 | + | end | |
| 95 | + | end | |
| 96 | + | end | |
| 97 | + | until not result.next(orders) | |
| 98 | + | result.free(orders) | |
| 99 | + | ||
| 100 | + | if not served then | |
| 101 | + | player:sendTextMessage(MESSAGE_STATUS, "You have no in-game orders to process.") | |
| 102 | + | end | |
| 103 | + | return true | |
| 104 | + | end | |
| 105 | + | ||
| 106 | + | znoteShopTalk:separator(" ") | |
| 107 | + | znoteShopTalk:register() |
| @@ -0,0 +1,68 @@ | |||
| 1 | + | -- Znote LoginWebService (version 1) for protocol 11, 12+ | |
| 2 | + | -- Move file to this location: data/scripts/znote_login.lua | |
| 3 | + | -- And restart OT server, it should auto load script. | |
| 4 | + | -- Requires updated version of Znote AAC. (18. June 2020) | |
| 5 | + | -- This script will help Znote AAC connect players to this game server. | |
| 6 | + | ||
| 7 | + | local znote_loginWebService = GlobalEvent("znote_loginWebService") | |
| 8 | + | function znote_loginWebService.onStartup() | |
| 9 | + | print(" ") | |
| 10 | + | print("=============================") | |
| 11 | + | print("= Znote AAC loginWebService =") | |
| 12 | + | print("=============================") | |
| 13 | + | local configLua = { | |
| 14 | + | ["SERVER_NAME"] = configManager.getString(configKeys.SERVER_NAME), | |
| 15 | + | ["IP"] = configManager.getString(configKeys.IP), | |
| 16 | + | ["GAME_PORT"] = configManager.getNumber(configKeys.GAME_PORT) | |
| 17 | + | } | |
| 18 | + | local configSQL = { | |
| 19 | + | ["SERVER_NAME"] = false, | |
| 20 | + | ["IP"] = false, | |
| 21 | + | ["GAME_PORT"] = false | |
| 22 | + | } | |
| 23 | + | local webStorage = db.storeQuery([[ | |
| 24 | + | SELECT | |
| 25 | + | `key`, | |
| 26 | + | `value` | |
| 27 | + | FROM `znote_global_storage` | |
| 28 | + | WHERE `key` IN('SERVER_NAME', 'IP', 'GAME_PORT') | |
| 29 | + | ]]) | |
| 30 | + | if webStorage ~= false then | |
| 31 | + | repeat | |
| 32 | + | local key = result.getString(webStorage, 'key') | |
| 33 | + | local value = result.getString(webStorage, 'value') | |
| 34 | + | configSQL[key] = value | |
| 35 | + | until not result.next(webStorage) | |
| 36 | + | result.free(webStorage) | |
| 37 | + | end | |
| 38 | + | local inserts = {} | |
| 39 | + | if configSQL.SERVER_NAME == false then | |
| 40 | + | table.insert(inserts, "('SERVER_NAME',".. db.escapeString(configLua.SERVER_NAME) ..")") | |
| 41 | + | elseif configSQL.SERVER_NAME ~= configLua.SERVER_NAME then | |
| 42 | + | db.query("UPDATE `znote_global_storage` SET `value`=".. db.escapeString(configLua.SERVER_NAME) .." WHERE `key`='SERVER_NAME';") | |
| 43 | + | print("= Updated [SERVER_NAME] FROM [" .. configSQL.SERVER_NAME .. "] to [" .. configLua.SERVER_NAME .. "]") | |
| 44 | + | end | |
| 45 | + | if configSQL.IP == false then | |
| 46 | + | table.insert(inserts, "('IP',".. db.escapeString(configLua.IP) ..")") | |
| 47 | + | elseif configSQL.IP ~= configLua.IP then | |
| 48 | + | db.query("UPDATE `znote_global_storage` SET `value`=".. db.escapeString(configLua.IP) .." WHERE `key`='IP';") | |
| 49 | + | print("= Updated [IP] FROM [" .. configSQL.IP .. "] to [" .. configLua.IP .. "]") | |
| 50 | + | end | |
| 51 | + | if configSQL.GAME_PORT == false then | |
| 52 | + | table.insert(inserts, "('GAME_PORT',".. db.escapeString(configLua.GAME_PORT) ..")") | |
| 53 | + | elseif configSQL.GAME_PORT ~= tostring(configLua.GAME_PORT) then | |
| 54 | + | db.query("UPDATE `znote_global_storage` SET `value`=".. db.escapeString(configLua.GAME_PORT) .." WHERE `key`='GAME_PORT';") | |
| 55 | + | print("= Updated [GAME_PORT] FROM [" .. configSQL.GAME_PORT .. "] to [" .. configLua.GAME_PORT .. "]") | |
| 56 | + | end | |
| 57 | + | if #inserts > 0 then | |
| 58 | + | db.query("INSERT INTO `znote_global_storage` (`key`,`value`) VALUES "..table.concat(inserts,',')..";") | |
| 59 | + | print("= Fixed " .. #inserts .. " missing configurations.") | |
| 60 | + | end | |
| 61 | + | print("=============================") | |
| 62 | + | print("= SERVER_NAME: " .. configLua.SERVER_NAME) | |
| 63 | + | print("= IP: " .. configLua.IP) | |
| 64 | + | print("= GAME_PORT: " .. configLua.GAME_PORT) | |
| 65 | + | print("=============================") | |
| 66 | + | print(" ") | |
| 67 | + | end | |
| 68 | + | znote_loginWebService:register() |
| @@ -0,0 +1,9 @@ | |||
| 1 | + | Add to data/talkactions/talkactions.xml: | |
| 2 | + | ||
| 3 | + | <talkaction words="!shop" script="znoteshop.lua"/> | |
| 4 | + | ||
| 5 | + | Then put znoteshop.lua in data/talkactions/scripts/ and restart the server. | |
| 6 | + | ||
| 7 | + | OTHire (7.72) has no mounts or addons, so only shop order types 1 (items) and | |
| 8 | + | 5 (outfit look type) are delivered in game. Everything else is handled by the | |
| 9 | + | website. |
| @@ -0,0 +1,76 @@ | |||
| 1 | + | -- Znote Shop for Znote AAC on OTHire (7.72). | |
| 2 | + | -- Player types !shop to receive the orders bought on the website. | |
| 3 | + | -- | |
| 4 | + | -- OTHire is a 7.72 server: there are no mounts and no addon system, so only | |
| 5 | + | -- order type 1 (items) and a plain type 5 (outfit look type, addon ignored by | |
| 6 | + | -- the client) are handled here. Premium / gender / name orders (2, 3, 4) are | |
| 7 | + | -- applied by the website. | |
| 8 | + | ||
| 9 | + | function onSay(cid, words, param) | |
| 10 | + | local storage = 54073 -- unused storage, throttles !shop against SQL spam | |
| 11 | + | local cooldown = 15 -- seconds | |
| 12 | + | ||
| 13 | + | if getPlayerStorageValue(cid, storage) > os.time() then | |
| 14 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "You can use !shop again in " .. (getPlayerStorageValue(cid, storage) - os.time()) .. " seconds.") | |
| 15 | + | return false | |
| 16 | + | end | |
| 17 | + | setPlayerStorageValue(cid, storage, os.time() + cooldown) | |
| 18 | + | ||
| 19 | + | local accid = getAccountNumberByPlayerName(getCreatureName(cid)) | |
| 20 | + | local orderQuery = db.storeQuery("SELECT `id`, `type`, `itemid`, `count` FROM `znote_shop_orders` WHERE `account_id` = " .. accid .. ";") | |
| 21 | + | if orderQuery == false then | |
| 22 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You have no orders.") | |
| 23 | + | return false | |
| 24 | + | end | |
| 25 | + | ||
| 26 | + | local served = false | |
| 27 | + | repeat | |
| 28 | + | local q_id = result.getDataInt(orderQuery, "id") | |
| 29 | + | local q_type = result.getDataInt(orderQuery, "type") | |
| 30 | + | local q_itemid = result.getDataInt(orderQuery, "itemid") | |
| 31 | + | local q_count = result.getDataInt(orderQuery, "count") | |
| 32 | + | ||
| 33 | + | -- ORDER TYPE 1 (items) | |
| 34 | + | if q_type == 1 then | |
| 35 | + | served = true | |
| 36 | + | if getPlayerFreeCap(cid) >= getItemWeight(q_itemid, q_count) then | |
| 37 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 38 | + | doPlayerAddItem(cid, q_itemid, q_count) | |
| 39 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received " .. q_count .. " " .. getItemName(q_itemid) .. "(s)!") | |
| 40 | + | else | |
| 41 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You need more capacity to carry this order!") | |
| 42 | + | end | |
| 43 | + | end | |
| 44 | + | ||
| 45 | + | -- ORDER TYPE 5 (outfit look type; addon bit is ignored on 7.72) | |
| 46 | + | if q_type == 5 then | |
| 47 | + | served = true | |
| 48 | + | local itemid = q_itemid | |
| 49 | + | local outfits = {} | |
| 50 | + | if itemid > 1000 then | |
| 51 | + | local first = math.floor(itemid / 1000) | |
| 52 | + | table.insert(outfits, first) | |
| 53 | + | itemid = itemid - (first * 1000) | |
| 54 | + | end | |
| 55 | + | table.insert(outfits, itemid) | |
| 56 | + | ||
| 57 | + | for _, outfitId in pairs(outfits) do | |
| 58 | + | if not canPlayerWearOutfit(cid, outfitId, q_count) then | |
| 59 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 60 | + | doPlayerAddOutfit(cid, outfitId, q_count) | |
| 61 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received a new outfit!") | |
| 62 | + | else | |
| 63 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You already have this outfit!") | |
| 64 | + | end | |
| 65 | + | end | |
| 66 | + | end | |
| 67 | + | ||
| 68 | + | -- Type 6 (mounts) and 7 (houses) do not exist on 7.72 / OTHire. | |
| 69 | + | until not result.next(orderQuery) | |
| 70 | + | result.free(orderQuery) | |
| 71 | + | ||
| 72 | + | if not served then | |
| 73 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "You have no orders to process in-game.") | |
| 74 | + | end | |
| 75 | + | return false | |
| 76 | + | end |
| @@ -0,0 +1,77 @@ | |||
| 1 | + | function onLogin(cid) | |
| 2 | + | local storage = 30055 -- storage value | |
| 3 | + | ||
| 4 | + | local sorcItems = { | |
| 5 | + | 2460, -- Brass helmet | |
| 6 | + | 2465, -- Brass armor | |
| 7 | + | 2190, -- Wand of vortex | |
| 8 | + | 2511, -- Brass shield | |
| 9 | + | 2478, -- Brass legs | |
| 10 | + | 2643, -- Leather boots | |
| 11 | + | 1988, -- Brown backpack | |
| 12 | + | 2050 -- torch | |
| 13 | + | } | |
| 14 | + | local druidItems = { | |
| 15 | + | 2460, -- Brass helmet | |
| 16 | + | 2465, -- Brass armor | |
| 17 | + | 2511, -- Brass shield | |
| 18 | + | 2182, -- Snakebite rod | |
| 19 | + | 2478, -- Brass legs | |
| 20 | + | 2643, -- Leather boots | |
| 21 | + | 1988, -- Brown backpack | |
| 22 | + | 2050 -- torch | |
| 23 | + | } | |
| 24 | + | local pallyItems = { | |
| 25 | + | 2460, -- Brass helmet | |
| 26 | + | 2465, -- Brass armor | |
| 27 | + | 2456, -- Bow | |
| 28 | + | 2478, -- Brass legs | |
| 29 | + | 2643, -- Leather boots | |
| 30 | + | 1988, -- Brown backpack | |
| 31 | + | } | |
| 32 | + | local kinaItems = { | |
| 33 | + | 2460, -- Brass helmet | |
| 34 | + | 2465, -- Brass armor | |
| 35 | + | 2511, -- Brass shield | |
| 36 | + | 2412, -- Katana | |
| 37 | + | 2478, -- Brass legs | |
| 38 | + | 2643, -- Leather boots | |
| 39 | + | 1988, -- Brown backpack | |
| 40 | + | 2050 -- torch | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | if getPlayerStorageValue(cid, storage) == -1 then | |
| 44 | + | setPlayerStorageValue(cid, storage, 1) | |
| 45 | + | if getPlayerVocation(cid) == 1 then | |
| 46 | + | -- Sorcerer | |
| 47 | + | for i = 1, table.getn(sorcItems), 1 do | |
| 48 | + | doPlayerAddItem(cid, sorcItems[i], 1, false) | |
| 49 | + | end | |
| 50 | + | ||
| 51 | + | elseif getPlayerVocation(cid) == 2 then | |
| 52 | + | -- Druid | |
| 53 | + | for i = 1, table.getn(druidItems), 1 do | |
| 54 | + | doPlayerAddItem(cid, druidItems[i], 1, false) | |
| 55 | + | end | |
| 56 | + | ||
| 57 | + | elseif getPlayerVocation(cid) == 3 then | |
| 58 | + | -- Paladin | |
| 59 | + | for i = 1, table.getn(pallyItems), 1 do | |
| 60 | + | doPlayerAddItem(cid, pallyItems[i], 1, false) | |
| 61 | + | end | |
| 62 | + | -- 8 arrows | |
| 63 | + | doPlayerAddItem(cid, 2544, 8, false) | |
| 64 | + | ||
| 65 | + | elseif getPlayerVocation(cid) == 4 then | |
| 66 | + | -- Knight | |
| 67 | + | for i = 1, table.getn(kinaItems), 1 do | |
| 68 | + | doPlayerAddItem(cid, kinaItems[i], 1, false) | |
| 69 | + | end | |
| 70 | + | end | |
| 71 | + | ||
| 72 | + | -- Common for all | |
| 73 | + | doPlayerAddItem(cid, 2674, 5, false) -- 5 apples | |
| 74 | + | doPlayerAddItem(cid, 2120, 1, false) -- 1 rope | |
| 75 | + | end | |
| 76 | + | return true | |
| 77 | + | end |
| @@ -0,0 +1,4 @@ | |||
| 1 | + | Step 1: Copy firstitems.lua to /data/creaturescripts/scripts/ folder | |
| 2 | + | -- Edit firstitems.lua with item IDs you want characters to start with on your server. | |
| 3 | + | ||
| 4 | + | Step 2: Restart OT server, and it should work. :) |
| @@ -0,0 +1,1 @@ | |||
| 1 | + | <talkaction words="!shop" script="znoteshop.lua"/> |
| @@ -0,0 +1,103 @@ | |||
| 1 | + | -- Znote Shop v1.1 for Znote AAC on TFS 0.2.13+ Mystic Spirit. | |
| 2 | + | function onSay(cid, words, param) | |
| 3 | + | local storage = 54073 -- Make sure to select non-used storage. This is used to prevent SQL load attacks. | |
| 4 | + | local cooldown = 15 -- in seconds. | |
| 5 | + | ||
| 6 | + | if getPlayerStorageValue(cid, storage) <= os.time() then | |
| 7 | + | setPlayerStorageValue(cid, storage, os.time() + cooldown) | |
| 8 | + | local accid = getAccountNumberByPlayerName(getCreatureName(cid)) | |
| 9 | + | ||
| 10 | + | local type_desc = { | |
| 11 | + | "itemids", | |
| 12 | + | "pending premium (skip)", | |
| 13 | + | "pending gender change (skip)", | |
| 14 | + | "pending character name change (skip)", | |
| 15 | + | "Outfit and addons", | |
| 16 | + | "Mounts", | |
| 17 | + | "Instant house purchase" | |
| 18 | + | } | |
| 19 | + | print("Player: " .. getCreatureName(cid) .. " triggered !shop talkaction.") | |
| 20 | + | -- Create the query | |
| 21 | + | local orderQuery = db.storeQuery("SELECT `id`, `type`, `itemid`, `count` FROM `znote_shop_orders` WHERE `account_id` = " .. accid .. ";") | |
| 22 | + | local served = false | |
| 23 | + | ||
| 24 | + | -- Detect if we got any results | |
| 25 | + | if orderQuery ~= false then | |
| 26 | + | -- Fetch order values | |
| 27 | + | local q_id = result.getDataInt(orderQuery, "id") | |
| 28 | + | local q_type = result.getDataInt(orderQuery, "type") | |
| 29 | + | local q_itemid = result.getDataInt(orderQuery, "itemid") | |
| 30 | + | local q_count = result.getDataInt(orderQuery, "count") | |
| 31 | + | ||
| 32 | + | local description = "Unknown or custom type" | |
| 33 | + | if type_desc[q_type] ~= nil then | |
| 34 | + | description = type_desc[q_type] | |
| 35 | + | end | |
| 36 | + | print("Processing type "..q_type..": ".. description) | |
| 37 | + | ||
| 38 | + | -- ORDER TYPE 1 (Regular item shop products) | |
| 39 | + | if q_type == 1 then | |
| 40 | + | served = true | |
| 41 | + | -- Get weight | |
| 42 | + | local playerCap = getPlayerFreeCap(cid) | |
| 43 | + | local itemweight = getItemWeight(q_itemid, q_count) | |
| 44 | + | if playerCap >= itemweight then | |
| 45 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 46 | + | doPlayerAddItem(cid, q_itemid, q_count) | |
| 47 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received ".. q_count .." "..getItemName(q_itemid).."(s)!") | |
| 48 | + | else | |
| 49 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "Need more CAP!") | |
| 50 | + | end | |
| 51 | + | end | |
| 52 | + | -- ORDER TYPE 5 (Outfit and addon) | |
| 53 | + | if q_type == 5 then | |
| 54 | + | served = true | |
| 55 | + | ||
| 56 | + | local itemid = q_itemid | |
| 57 | + | local outfits = {} | |
| 58 | + | ||
| 59 | + | if itemid > 1000 then | |
| 60 | + | local first = math.floor(itemid/1000) | |
| 61 | + | table.insert(outfits, first) | |
| 62 | + | itemid = itemid - (first * 1000) | |
| 63 | + | end | |
| 64 | + | table.insert(outfits, itemid) | |
| 65 | + | ||
| 66 | + | for _, outfitId in pairs(outfits) do | |
| 67 | + | -- Make sure player don't already have this outfit and addon | |
| 68 | + | if not canPlayerWearOutfit(cid, outfitId, q_count) then | |
| 69 | + | db.query("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 70 | + | doPlayerAddOutfit(cid,outfitId,q_count) | |
| 71 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received a new outfit!") | |
| 72 | + | else | |
| 73 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You already have this outfit and addon!") | |
| 74 | + | end | |
| 75 | + | end | |
| 76 | + | end | |
| 77 | + | ||
| 78 | + | -- ORDER TYPE 6 (Mounts) | |
| 79 | + | -- Not supported on TFS 0.2 | |
| 80 | + | ||
| 81 | + | -- Add custom order types here | |
| 82 | + | -- Type 1 is for itemids (Already coded here) | |
| 83 | + | -- Type 2 is for premium (Coded on web) | |
| 84 | + | -- Type 3 is for gender change (Coded on web) | |
| 85 | + | -- Type 4 is for character name change (Coded on web) | |
| 86 | + | -- Type 5 is for character outfit and addon (Already coded here) | |
| 87 | + | -- Type 6 is for mounts (Not for TFS 0.2) | |
| 88 | + | -- Type 7 is for Instant house purchase (Not for TFS 0.2) | |
| 89 | + | -- So use type 8+ for custom stuff, like etc packages. | |
| 90 | + | -- if q_type == 8 then | |
| 91 | + | -- end | |
| 92 | + | result.free(orderQuery) | |
| 93 | + | if not served then | |
| 94 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "You have no orders to process in-game.") | |
| 95 | + | end | |
| 96 | + | else | |
| 97 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You have no orders.") | |
| 98 | + | end | |
| 99 | + | else | |
| 100 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "Can only be executed once every "..cooldown.." seconds. Remaining cooldown: ".. getPlayerStorageValue(cid, storage) - os.time()) | |
| 101 | + | end | |
| 102 | + | return false | |
| 103 | + | end |
| @@ -0,0 +1,77 @@ | |||
| 1 | + | function onLogin(cid) | |
| 2 | + | local storage = 30055 -- storage value | |
| 3 | + | ||
| 4 | + | local sorcItems = { | |
| 5 | + | 2460, -- Brass helmet | |
| 6 | + | 2465, -- Brass armor | |
| 7 | + | 2190, -- Wand of vortex | |
| 8 | + | 2511, -- Brass shield | |
| 9 | + | 2478, -- Brass legs | |
| 10 | + | 2643, -- Leather boots | |
| 11 | + | 1988, -- Brown backpack | |
| 12 | + | 2050 -- torch | |
| 13 | + | } | |
| 14 | + | local druidItems = { | |
| 15 | + | 2460, -- Brass helmet | |
| 16 | + | 2465, -- Brass armor | |
| 17 | + | 2511, -- Brass shield | |
| 18 | + | 2182, -- Snakebite rod | |
| 19 | + | 2478, -- Brass legs | |
| 20 | + | 2643, -- Leather boots | |
| 21 | + | 1988, -- Brown backpack | |
| 22 | + | 2050 -- torch | |
| 23 | + | } | |
| 24 | + | local pallyItems = { | |
| 25 | + | 2460, -- Brass helmet | |
| 26 | + | 2465, -- Brass armor | |
| 27 | + | 2456, -- Bow | |
| 28 | + | 2478, -- Brass legs | |
| 29 | + | 2643, -- Leather boots | |
| 30 | + | 1988, -- Brown backpack | |
| 31 | + | } | |
| 32 | + | local kinaItems = { | |
| 33 | + | 2460, -- Brass helmet | |
| 34 | + | 2465, -- Brass armor | |
| 35 | + | 2511, -- Brass shield | |
| 36 | + | 2412, -- Katana | |
| 37 | + | 2478, -- Brass legs | |
| 38 | + | 2643, -- Leather boots | |
| 39 | + | 1988, -- Brown backpack | |
| 40 | + | 2050 -- torch | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | if getPlayerStorageValue(cid, storage) == -1 then | |
| 44 | + | setPlayerStorageValue(cid, storage, 1) | |
| 45 | + | if getPlayerVocation(cid) == 1 then | |
| 46 | + | -- Sorcerer | |
| 47 | + | for i = 1, table.getn(sorcItems), 1 do | |
| 48 | + | doPlayerAddItem(cid, sorcItems[i], 1, false) | |
| 49 | + | end | |
| 50 | + | ||
| 51 | + | elseif getPlayerVocation(cid) == 2 then | |
| 52 | + | -- Druid | |
| 53 | + | for i = 1, table.getn(druidItems), 1 do | |
| 54 | + | doPlayerAddItem(cid, druidItems[i], 1, false) | |
| 55 | + | end | |
| 56 | + | ||
| 57 | + | elseif getPlayerVocation(cid) == 3 then | |
| 58 | + | -- Paladin | |
| 59 | + | for i = 1, table.getn(pallyItems), 1 do | |
| 60 | + | doPlayerAddItem(cid, pallyItems[i], 1, false) | |
| 61 | + | end | |
| 62 | + | -- 8 arrows | |
| 63 | + | doPlayerAddItem(cid, 2544, 8, false) | |
| 64 | + | ||
| 65 | + | elseif getPlayerVocation(cid) == 4 then | |
| 66 | + | -- Knight | |
| 67 | + | for i = 1, table.getn(kinaItems), 1 do | |
| 68 | + | doPlayerAddItem(cid, kinaItems[i], 1, false) | |
| 69 | + | end | |
| 70 | + | end | |
| 71 | + | ||
| 72 | + | -- Common for all | |
| 73 | + | doPlayerAddItem(cid, 2674, 5, false) -- 5 apples | |
| 74 | + | doPlayerAddItem(cid, 2120, 1, false) -- 1 rope | |
| 75 | + | end | |
| 76 | + | return true | |
| 77 | + | end |
| @@ -0,0 +1,10 @@ | |||
| 1 | + | Step 1: Copy firstitems.lua to /data/creaturescripts/scripts/ folder | |
| 2 | + | -- Edit firstitems.lua with item IDs you want characters to start with on your server. | |
| 3 | + | ||
| 4 | + | Step 2: Edit the /data/creaturescripts/creaturescripts.XML file | |
| 5 | + | - ADD: <event type="login" name="firstItems" event="script" value="firstitems.lua"/> | |
| 6 | + | ||
| 7 | + | Step 3: Edit the /data/creaturescripts/scripts/login.lua file | |
| 8 | + | - ADD: registerCreatureEvent(cid, "firstItems") | |
| 9 | + | ||
| 10 | + | Step 4: Restart OT server, and it should work. :) |
| @@ -0,0 +1,7 @@ | |||
| 1 | + | 1. Add below line to XML file: data/creaturescripts/creaturescripts.xml | |
| 2 | + | <event type="login" name="znote_syncoutfits" event="script" value="syncoutfit.lua"/> | |
| 3 | + | ||
| 4 | + | 2. Register event in login.lua: data/creaturescripts/scripts/login.lua | |
| 5 | + | registerCreatureEvent(cid, "znote_syncoutfits") | |
| 6 | + | ||
| 7 | + | 3. Place Lua file syncoutfit.lua in folder: data/creaturescripts/scripts/ |
| @@ -0,0 +1,39 @@ | |||
| 1 | + | -- Sync outfits that player own with Znote AAC | |
| 2 | + | -- So its possible to see which full sets player | |
| 3 | + | -- has in characterprofile.php | |
| 4 | + | ||
| 5 | + | znote_outfit_list = { | |
| 6 | + | { -- Female (girl) outfits | |
| 7 | + | 136,137,138,139,140,141,142,147,148, | |
| 8 | + | 149,150,155,156,157,158,252,269,270, | |
| 9 | + | 279,288,324,329,336,366,431,433,464, | |
| 10 | + | 466,471,513,514,542,575,578,618,620, | |
| 11 | + | 632,635,636,664,666,683,694,696,698, | |
| 12 | + | 724,732,745,749,759,845,852,874,885, | |
| 13 | + | 900 | |
| 14 | + | }, | |
| 15 | + | { -- Male (boy) outfits | |
| 16 | + | 128,129,130,131,132,133,134,143,144, | |
| 17 | + | 145,146,151,152,153,154,251,268,273, | |
| 18 | + | 278,289,325,328,335,367,430,432,463, | |
| 19 | + | 465,472,512,516,541,574,577,610,619, | |
| 20 | + | 633,634,637,665,667,684,695,697,699, | |
| 21 | + | 725,733,746,750,760,846,853,873,884, | |
| 22 | + | 899 | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | function onLogin(cid) | |
| 27 | + | -- storage_value + 1000 storages (highest outfit id) must not be used in other script. | |
| 28 | + | -- Must be identical to Znote AAC config.php: $config['EQ_shower'] -> storage_value | |
| 29 | + | local storage_value = 10000 | |
| 30 | + | -- Loop through outfits | |
| 31 | + | for _, outfit in pairs(znote_outfit_list[getPlayerSex(cid)+1]) do | |
| 32 | + | if canPlayerWearOutfit(cid,outfit,3) then | |
| 33 | + | if getPlayerStorageValue(cid,storage_value + outfit) ~= 3 then | |
| 34 | + | setPlayerStorageValue(cid,storage_value + outfit, 3) | |
| 35 | + | end | |
| 36 | + | end | |
| 37 | + | end | |
| 38 | + | return true | |
| 39 | + | end |
| @@ -0,0 +1,118 @@ | |||
| 1 | + | -- Znote Shop v1.1 for Znote AAC on TFS 0.3.6+ Crying Damson. [Alternative] | |
| 2 | + | function onSay(cid, words, param) | |
| 3 | + | local storage = 54073 -- Make sure to select non-used storage. This is used to prevent SQL load attacks. | |
| 4 | + | local cooldown = 15 -- in seconds. | |
| 5 | + | ||
| 6 | + | if getPlayerStorageValue(cid, storage) <= os.time() then | |
| 7 | + | setPlayerStorageValue(cid, storage, os.time() + cooldown) | |
| 8 | + | local accid = getAccountNumberByPlayerName(getCreatureName(cid)) | |
| 9 | + | ||
| 10 | + | local type_desc = { | |
| 11 | + | "itemids", | |
| 12 | + | "pending premium (skip)", | |
| 13 | + | "pending gender change (skip)", | |
| 14 | + | "pending character name change (skip)", | |
| 15 | + | "Outfit and addons", | |
| 16 | + | "Mounts", | |
| 17 | + | "Instant house purchase" | |
| 18 | + | } | |
| 19 | + | print("Player: " .. getCreatureName(cid) .. " triggered !shop talkaction.") | |
| 20 | + | -- Create the query | |
| 21 | + | local orderQuery = db.storeQuery("SELECT `id`, `type`, `itemid`, `count` FROM `znote_shop_orders` WHERE `account_id` = " .. accid .. ";") | |
| 22 | + | local served = false | |
| 23 | + | ||
| 24 | + | -- Detect if we got any results | |
| 25 | + | if orderQuery ~= false then | |
| 26 | + | repeat | |
| 27 | + | -- Fetch order values | |
| 28 | + | local q_id = result.getDataInt(orderQuery, "id") | |
| 29 | + | local q_type = result.getDataInt(orderQuery, "type") | |
| 30 | + | local q_itemid = result.getDataInt(orderQuery, "itemid") | |
| 31 | + | local q_count = result.getDataInt(orderQuery, "count") | |
| 32 | + | ||
| 33 | + | local description = "Unknown or custom type" | |
| 34 | + | if type_desc[q_type] ~= nil then | |
| 35 | + | description = type_desc[q_type] | |
| 36 | + | end | |
| 37 | + | print("Processing type "..q_type..": ".. description) | |
| 38 | + | ||
| 39 | + | -- ORDER TYPE 1 (Regular item shop products) | |
| 40 | + | if q_type == 1 then | |
| 41 | + | served = true | |
| 42 | + | -- Get wheight | |
| 43 | + | local playerCap = getPlayerFreeCap(cid) | |
| 44 | + | local itemweight = getItemWeightById(q_itemid, q_count) | |
| 45 | + | if playerCap >= itemweight then | |
| 46 | + | local delete = db.storeQuery("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 47 | + | result.free(delete) | |
| 48 | + | doPlayerAddItem(cid, q_itemid, q_count) | |
| 49 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have recieved ".. q_count .." "..getItemNameById(q_itemid).."(s)!") | |
| 50 | + | else | |
| 51 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "Need more CAP!") | |
| 52 | + | end | |
| 53 | + | end | |
| 54 | + | -- ORDER TYPE 5 (Outfit and addon) | |
| 55 | + | if q_type == 5 then | |
| 56 | + | served = true | |
| 57 | + | ||
| 58 | + | local itemid = q_itemid | |
| 59 | + | local outfits = {} | |
| 60 | + | ||
| 61 | + | if itemid > 1000 then | |
| 62 | + | local first = math.floor(itemid/1000) | |
| 63 | + | table.insert(outfits, first) | |
| 64 | + | itemid = itemid - (first * 1000) | |
| 65 | + | end | |
| 66 | + | table.insert(outfits, itemid) | |
| 67 | + | ||
| 68 | + | for _, outfitId in pairs(outfits) do | |
| 69 | + | -- Make sure player don't already have this outfit and addon | |
| 70 | + | if not canPlayerWearOutfit(cid, outfitId, q_count) then | |
| 71 | + | local delete = db.storeQuery("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 72 | + | result.free(delete) | |
| 73 | + | doPlayerAddOutfit(cid,outfitId,q_count) | |
| 74 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received a new outfit!") | |
| 75 | + | else | |
| 76 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You already have this outfit and addon!") | |
| 77 | + | end | |
| 78 | + | end | |
| 79 | + | end | |
| 80 | + | ||
| 81 | + | -- ORDER TYPE 6 (Mounts) | |
| 82 | + | if q_type == 6 then | |
| 83 | + | served = true | |
| 84 | + | -- Make sure player don't already have this outfit and addon | |
| 85 | + | if not getPlayerMount(cid, q_itemid) then -- Failed to find a proper hasMount 0.3 function? | |
| 86 | + | local delete = db.storeQuery("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 87 | + | result.free(delete) | |
| 88 | + | doPlayerAddMount(cid, q_itemid) | |
| 89 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received a new mount!") | |
| 90 | + | else | |
| 91 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You already have this mount!") | |
| 92 | + | end | |
| 93 | + | end | |
| 94 | + | ||
| 95 | + | -- Add custom order types here | |
| 96 | + | -- Type 1 is for itemids (Already coded here) | |
| 97 | + | -- Type 2 is for premium (Coded on web) | |
| 98 | + | -- Type 3 is for gender change (Coded on web) | |
| 99 | + | -- Type 4 is for character name change (Coded on web) | |
| 100 | + | -- Type 5 is for character outfit and addon (Already coded here) | |
| 101 | + | -- Type 6 is for mounts (Already coded here) | |
| 102 | + | -- Type 7 is for Instant house purchase (Not for TFS 0.3) | |
| 103 | + | -- So use type 8+ for custom stuff, like etc packages. | |
| 104 | + | -- if q_type == 8 then | |
| 105 | + | -- end | |
| 106 | + | until not result.next(orderQuery) | |
| 107 | + | result.free(orderQuery) | |
| 108 | + | if not served then | |
| 109 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "You have no orders to process in-game.") | |
| 110 | + | end | |
| 111 | + | else | |
| 112 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You have no orders.") | |
| 113 | + | end | |
| 114 | + | else | |
| 115 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "Can only be executed once every "..cooldown.." seconds. Remaining cooldown: ".. getPlayerStorageValue(cid, storage) - os.time()) | |
| 116 | + | end | |
| 117 | + | return false | |
| 118 | + | end |
| @@ -0,0 +1,1 @@ | |||
| 1 | + | <talkaction words="!shop" event="script" value="znoteshop.lua"/> |
| @@ -0,0 +1,128 @@ | |||
| 1 | + | -- Znote Shop v1.1 for Znote AAC on TFS 0.3.6+ Crying Damson. | |
| 2 | + | function onSay(cid, words, param) | |
| 3 | + | local storage = 54073 -- Make sure to select non-used storage. This is used to prevent SQL load attacks. | |
| 4 | + | local cooldown = 15 -- in seconds. | |
| 5 | + | ||
| 6 | + | if getPlayerStorageValue(cid, storage) <= os.time() then | |
| 7 | + | setPlayerStorageValue(cid, storage, os.time() + cooldown) | |
| 8 | + | local accid = getAccountNumberByPlayerName(getCreatureName(cid)) | |
| 9 | + | ||
| 10 | + | local type_desc = { | |
| 11 | + | "itemids", | |
| 12 | + | "pending premium (skip)", | |
| 13 | + | "pending gender change (skip)", | |
| 14 | + | "pending character name change (skip)", | |
| 15 | + | "Outfit and addons", | |
| 16 | + | "Mounts", | |
| 17 | + | "Instant house purchase" | |
| 18 | + | } | |
| 19 | + | print("Player: " .. getCreatureName(cid) .. " triggered !shop talkaction.") | |
| 20 | + | -- Create the query | |
| 21 | + | local orderQuery = db.storeQuery("SELECT `id`, `type`, `itemid`, `count` FROM `znote_shop_orders` WHERE `account_id` = " .. accid .. ";") | |
| 22 | + | local served = false | |
| 23 | + | ||
| 24 | + | -- Detect if we got any results | |
| 25 | + | if orderQuery ~= false then | |
| 26 | + | repeat | |
| 27 | + | -- Fetch order values | |
| 28 | + | local q_id = result.getDataInt(orderQuery, "id") | |
| 29 | + | local q_type = result.getDataInt(orderQuery, "type") | |
| 30 | + | local q_itemid = result.getDataInt(orderQuery, "itemid") | |
| 31 | + | local q_count = result.getDataInt(orderQuery, "count") | |
| 32 | + | ||
| 33 | + | local description = "Unknown or custom type" | |
| 34 | + | if type_desc[q_type] ~= nil then | |
| 35 | + | description = type_desc[q_type] | |
| 36 | + | end | |
| 37 | + | print("Processing type "..q_type..": ".. description) | |
| 38 | + | ||
| 39 | + | -- ORDER TYPE 1 (Regular item shop products) | |
| 40 | + | if q_type == 1 then | |
| 41 | + | served = true | |
| 42 | + | -- Get weight | |
| 43 | + | local playerCap = getPlayerFreeCap(cid) | |
| 44 | + | local itemweight = getItemWeightById(q_itemid, q_count) | |
| 45 | + | if playerCap >= itemweight and getTileInfo(getCreaturePosition(cid)).protection then | |
| 46 | + | -- backpack check | |
| 47 | + | local backpack = getPlayerSlotItem(cid, 3) | |
| 48 | + | local gotItem = false | |
| 49 | + | if(backpack and backpack.itemid > 0) then | |
| 50 | + | local received = doAddContainerItem(getPlayerSlotItem(cid, 3).uid, q_itemid,q_count) | |
| 51 | + | if(received ~= false) then | |
| 52 | + | db.executeQuery("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 53 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received ".. q_count .." "..getItemNameById(q_itemid).."(s)!") | |
| 54 | + | gotItem = true | |
| 55 | + | end | |
| 56 | + | end | |
| 57 | + | ||
| 58 | + | if(not gotItem) then | |
| 59 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You have no available space in backpack to receive that item.") | |
| 60 | + | end | |
| 61 | + | else | |
| 62 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "Need more CAP and Need ProtectZone!") | |
| 63 | + | end | |
| 64 | + | end | |
| 65 | + | -- ORDER TYPE 5 (Outfit and addon) | |
| 66 | + | if q_type == 5 then | |
| 67 | + | served = true | |
| 68 | + | ||
| 69 | + | local itemid = q_itemid | |
| 70 | + | local outfits = {} | |
| 71 | + | ||
| 72 | + | if itemid > 1000 then | |
| 73 | + | local first = math.floor(itemid/1000) | |
| 74 | + | table.insert(outfits, first) | |
| 75 | + | itemid = itemid - (first * 1000) | |
| 76 | + | end | |
| 77 | + | table.insert(outfits, itemid) | |
| 78 | + | ||
| 79 | + | for _, outfitId in pairs(outfits) do | |
| 80 | + | -- Make sure player don't already have this outfit and addon | |
| 81 | + | if not canPlayerWearOutfit(cid, outfitId, q_count) then | |
| 82 | + | db.executeQuery("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 83 | + | doPlayerAddOutfit(cid,outfitId,q_count) | |
| 84 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received a new outfit!") | |
| 85 | + | else | |
| 86 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You already have this outfit and addon!") | |
| 87 | + | end | |
| 88 | + | end | |
| 89 | + | end | |
| 90 | + | ||
| 91 | + | -- ORDER TYPE 6 (Mounts) | |
| 92 | + | if q_type == 6 then | |
| 93 | + | served = true | |
| 94 | + | -- Make sure player don't already have this outfit and addon | |
| 95 | + | if not getPlayerMount(cid, q_itemid) then -- Failed to find a proper hasMount 0.3 function? | |
| 96 | + | db.executeQuery("DELETE FROM `znote_shop_orders` WHERE `id` = " .. q_id .. ";") | |
| 97 | + | doPlayerAddMount(cid, q_itemid) | |
| 98 | + | doPlayerSendTextMessage(cid, MESSAGE_INFO_DESCR, "Congratulations! You have received a new mount!") | |
| 99 | + | else | |
| 100 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_WARNING, "You already have this mount!") | |
| 101 | + | end | |
| 102 | + | end | |
| 103 | + | ||
| 104 | + | -- Add custom order types here | |
| 105 | + | -- Type 1 is for itemids (Already coded here) | |
| 106 | + | -- Type 2 is for premium (Coded on web) | |
| 107 | + | -- Type 3 is for gender change (Coded on web) | |
| 108 | + | -- Type 4 is for character name change (Coded on web) | |
| 109 | + | -- Type 5 is for character outfit and addon (Already coded here) | |
| 110 | + | -- Type 6 is for mounts (Already coded here) | |
| 111 | + | -- Type 7 is for Instant house purchase (Not for TFS 0.3) | |
| 112 | + | -- So use type 8+ for custom stuff, like etc packages. | |
| 113 | + | -- if q_type == 8 then | |
| 114 | + | -- end | |
| 115 | + | until not result.next(orderQuery) | |
| 116 | + | result.free(orderQuery) | |
| 117 | + | if not served then | |
| 118 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "You have no orders to process in-game.") | |
| 119 | + | end | |
| 120 | + | else | |
| 121 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "You have no orders.") | |
| 122 | + | end | |
| 123 | + | ||
| 124 | + | else | |
| 125 | + | doPlayerSendTextMessage(cid, MESSAGE_STATUS_CONSOLE_BLUE, "Can only be executed once every "..cooldown.." seconds. Remaining cooldown: ".. getPlayerStorageValue(cid, storage) - os.time()) | |
| 126 | + | end | |
| 127 | + | return false | |
| 128 | + | end |
| @@ -0,0 +1,114 @@ | |||
| 1 | + | -- With Rookgaard | |
| 2 | + | ||
| 3 | + | --[[ | |
| 4 | + | local firstItems = {2050, 2382} -- torch and club | |
| 5 | + | ||
| 6 | + | function onLogin(player) | |
| 7 | + | if player:getLastLoginSaved() <= 0 then | |
| 8 | + | for i = 1, #firstItems do | |
| 9 | + | player:addItem(firstItems[i], 1) | |
| 10 | + | end | |
| 11 | + | player:addItem(player:getSex() == 0 and 2651 or 2650, 1) -- coat | |
| 12 | + | player:addItem(ITEM_BAG, 1) | |
| 13 | + | player:addItem(2674, 1) -- red apple | |
| 14 | + | end | |
| 15 | + | return true | |
| 16 | + | end | |
| 17 | + | ]]-- | |
| 18 | + | ||
| 19 | + | -- Without Rookgaard | |
| 20 | + | local config = { | |
| 21 | + | [1] = { -- Sorcerer | |
| 22 | + | items = { | |
| 23 | + | {2175, 1}, -- spellbook | |
| 24 | + | {2190, 1}, -- wand of vortex | |
| 25 | + | {8819, 1}, -- magician's robe | |
| 26 | + | {8820, 1}, -- mage hat | |
| 27 | + | {2468, 1}, -- studded legs | |
| 28 | + | {2643, 1}, -- leather boots | |
| 29 | + | {2661, 1} -- scarf | |
| 30 | + | }, | |
| 31 | + | container = { | |
| 32 | + | {2120, 1}, -- rope | |
| 33 | + | {2554, 1}, -- shovel | |
| 34 | + | {7620, 1} -- mana potion | |
| 35 | + | } | |
| 36 | + | }, | |
| 37 | + | [2] = { -- Druid | |
| 38 | + | items = { | |
| 39 | + | {2175, 1}, -- spellbook | |
| 40 | + | {2182, 1}, -- snakebite rod | |
| 41 | + | {8819, 1}, -- magician's robe | |
| 42 | + | {8820, 1}, -- mage hat | |
| 43 | + | {2468, 1}, -- studded legs | |
| 44 | + | {2643, 1}, -- leather boots | |
| 45 | + | {2661, 1} -- scarf | |
| 46 | + | }, | |
| 47 | + | container = { | |
| 48 | + | {2120, 1}, -- rope | |
| 49 | + | {2554, 1}, -- shovel | |
| 50 | + | {7620, 1} -- mana potion | |
| 51 | + | } | |
| 52 | + | }, | |
| 53 | + | [3] = { -- Paladin | |
| 54 | + | items = { | |
| 55 | + | {2525, 1}, -- dwarven shield | |
| 56 | + | {2389, 5}, -- 5 spears | |
| 57 | + | {2660, 1}, -- ranger's cloak | |
| 58 | + | {8923, 1}, -- ranger legs | |
| 59 | + | {2643, 1}, -- leather boots | |
| 60 | + | {2661, 1}, -- scarf | |
| 61 | + | {2480, 1} -- legion helmet | |
| 62 | + | }, | |
| 63 | + | container = { | |
| 64 | + | {2120, 1}, -- rope | |
| 65 | + | {2554, 1}, -- shovel | |
| 66 | + | {7618, 1}, -- health potion | |
| 67 | + | {2456, 1}, -- bow | |
| 68 | + | {2544, 50} -- 50 arrows | |
| 69 | + | } | |
| 70 | + | }, | |
| 71 | + | [4] = { -- Knight | |
| 72 | + | items = { | |
| 73 | + | {2525, 1}, -- dwarven shield | |
| 74 | + | {8601, 1}, -- steel axe | |
| 75 | + | {2465, 1}, -- brass armor | |
| 76 | + | {2460, 1}, -- brass helmet | |
| 77 | + | {2478, 1}, -- brass legs | |
| 78 | + | {2643, 1}, -- leather boots | |
| 79 | + | {2661, 1} -- scarf | |
| 80 | + | }, | |
| 81 | + | container = { | |
| 82 | + | {8602, 1}, -- jagged sword | |
| 83 | + | {2439, 1}, -- daramanian mace | |
| 84 | + | {2120, 1}, -- rope | |
| 85 | + | {2554, 1}, -- shovel | |
| 86 | + | {7618, 1} -- health potion | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | function onLogin(player) | |
| 92 | + | local targetVocation = config[player:getVocation():getId()] | |
| 93 | + | if not targetVocation then | |
| 94 | + | return true | |
| 95 | + | end | |
| 96 | + | ||
| 97 | + | if player:getLastLoginSaved() ~= 0 then | |
| 98 | + | return true | |
| 99 | + | end | |
| 100 | + | ||
| 101 | + | for i = 1, #targetVocation.items do | |
| 102 | + | player:addItem(targetVocation.items[i][1], targetVocation.items[i][2]) | |
| 103 | + | end | |
| 104 | + | ||
| 105 | + | local backpack = player:addItem(1988) | |
| 106 | + | if not backpack then | |
| 107 | + | return true | |
| 108 | + | end | |
| 109 | + | ||
| 110 | + | for i = 1, #targetVocation.container do | |
| 111 | + | backpack:addItem(targetVocation.container[i][1], targetVocation.container[i][2]) | |
| 112 | + | end | |
| 113 | + | return true | |
| 114 | + | end |
| @@ -0,0 +1,4 @@ | |||
| 1 | + | Step 1: Copy firstitems.lua to /data/creaturescripts/scripts/ folder | |
| 2 | + | -- Edit firstitems.lua with item IDs you want characters to start with on your server. | |
| 3 | + | ||
| 4 | + | Step 2: Restart OT server, and it should work. :) |
| @@ -0,0 +1,3 @@ | |||
| 1 | + | Step 1: Replace the one that comes with tfs with this one | |
| 2 | + | ||
| 3 | + | Step 2: Restart OT server, and it should work. :) |