• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

TFS 1.x Ingame Account Manager [Searching Testers] UPDATED

I would like to have some feedback on my recovery key system, I'm not sure if this could be done in a better way with a similiar or as good result
@Summ @zbizu @Flatlander @cbrm @StreamSide @Evil Puncker @EvilSkillz @Ninja @Limos @Evan sorry for tagging you guys, if you don't have the time / interest it's fine but I would really appreciate some feedback :)

What I basicly do is.. I create a 9 digit letter key for the encryption (I use uppercase and lowercase)
Code:
function generateEncryptionKey()
   local key = {}
   local n = ""
   repeat
     local n = math.random(string.byte("A"), string.byte("z"))
     if isInArray({91,92,93,94,95,96}, n) or isInArray(key, string.char(n)) then
       repeat
         n = math.random(string.byte("A"), string.byte("z"))
       until not isInArray({91,92,93,94,95,96}, n) and not isInArray(key, string.char(n))
     end
     table.insert(key, string.char(n))
   until #key == 9
   return key
end
I save this key into the database (creating a new table etc. therefore)
Code:
function saveEncryptionKey()
   local key = generateEncryptionKey()
   db.query("CREATE TABLE IF NOT EXISTS `recovery_key` (`id` tinyint(1) NOT NULL,`key1` varchar(1) NOT NULL,`key2` varchar(1) NOT NULL,`key3` varchar(1) NOT NULL,`key4` varchar(1) NOT NULL,`key5` varchar(1) NOT NULL,`key6` varchar(1) NOT NULL,`key7` varchar(1) NOT NULL,`key8` varchar(1) NOT NULL,`key9` varchar(1) NOT NULL,UNIQUE KEY (`id`)) ENGINE=InnoDB;")
   local res = db.storeQuery("SELECT * FROM `recovery_key` WHERE `id` = 1")
   if not res then
     db.asyncQuery("INSERT INTO `recovery_key`(`id`, `key1`, `key2`, `key3`, `key4`, `key5`, `key6`, `key7`, `key8`, `key9`) VALUES (1,'".. key[1] .."','".. key[2] .."','".. key[3] .."','".. key[4] .."','".. key[5] .."','".. key[6] .."','".. key[7] .."','".. key[8] .."','".. key[9] .."')")
   end
   result.free(res)
end
once the server starts I load the key from the database
Code:
function loadEncryptionKey()
   local res = db.storeQuery("SELECT `key1`, `key2`, `key3`, `key4`, `key5`, `key6`, `key7`, `key8`, `key9` FROM `recovery_key` WHERE `id` = 1")
   local keys = {"key1","key2","key3","key4","key5","key6","key7","key8","key9"}
   local encrypt = {}
   if res then
     for k, v in pairs(keys) do
       table.insert(encrypt, result.getDataString(res, v))
     end
   end
   result.free()
   return encrypt
end

Let's say our encryption key looks like this now
Code:
encryptionKey = {"G","z","Y","E","q","K","u","i","L"}

Now let's take a look on how I create the recovery keys for the players
a key consists of exactly 15 character length, I use the 2 letters in each key as indicators.
Code:
function createRecoveryKey()
   local key = ""
   math.randomseed(os.time())
   local seq1 = math.random(3,9)
   math.randomseed(os.time()*os.time())
   local seq2 = math.random(3,9)
   key = tostring(math.floor((seq1 ^ seq2)))
   key = key .."".. encryptionKey[seq1] .."".. encryptionKey[seq2]
   repeat
     local rnd = math.random(0,9)
     key = key .."".. tostring(rnd)
   until string.len(key) >= 15
   return key
end

and this is how I search for the valid sequence in the recovery key
Code:
function isValidRecoveryKeySequence(playerId, key)
   local gKey = ""
   local t = {}; local p = {}; q = ""
   if string.find(key, "%a") then
     local a = string.find(key, "%a")
     local z = ""
     for i = 1, a-1 do
       local c = key:sub(i,i)
       q = q .."".. c
     end
     for i = a, a+1 do
       local c = key:sub(i,i)
       table.insert(t, c)
     end
     if not isInArray(encryptionKey, t[1]) or not isInArray(encryptionKey, t[2]) then
       return false
     end
     for y, x in pairs(t) do
       for k, v in pairs(encryptionKey) do
         if x == v then
           table.insert(p, k)
         end
       end
     end
     gKey = math.floor((p[1] ^ p[2])) .."".. t[1] .."".. t[2]
     if string.find(key, gKey) then
       return true
     end
   end
   return false
end

I just need to know if what I'm doing is not efficient and can be cracked easily or if I approached that topic in the right way.
 
Last edited:
use encryption like for passwords(it was sha1 or sha256, can't remember)
generate it once, send to player, put encrypted value to database
if encrypted value == encrypted reckey from database then key is correct end
you may use numbers for improved safety
 
use encryption like for passwords(it was sha1 or sha256, can't remember)
generate it once, send to player, put encrypted value to database
if encrypted value == encrypted reckey from database then key is correct end
you may use numbers for improved safety
well I can still encrypt my current recovery keys into sha1 that wouldn't be the issue, however if I just randomly generate numbers and encrypt them, then I have certainly no way to check beforehand if the key could be valid in it's sequence or not, thus I would have to check them always straight with the one from the database and that's what I not wanted to do actually, keeping database connection to a minimum.
 
I have been working on optimization lately and some rough configuration for the account manager.
Code:
accountManagerConfig =
{
   enabled = true, -- true = You can use the account manager, false = You cannot use the account manager.
   maxCharacters = 10, -- means how much characters each account can have at max.
   kickWithSameIP = true, -- false = if there are more account managers online with the same IP it wont kick them, true = It'll allow only a set ammount of account managers to be logged in with the same IP at once.
   kickAtAmmount = 2, -- this only take in place if "kickWithSameIP" = true; 2 = only two account managers can be logged in with the same IP at once.
   canChoseVoc = true, -- true = your player can chose which voc he wants for his char (start at lvl 8), false = you have no voc and start at lvl 1.
   townID = 1, -- which town the player will be at first login.
   voc = -- put all the starting vocations in here, with the correct id of them.
   {
     [1] = "sorcerer",
     [2] = "druid",
     [3] = "paladin",
     [4] = "knight"
   },
   startDest = -- destination where the account manager spawns at (I recommend a place where he is alone and no other players can go to)
   {
     x = 880,
     y = 1440,
     z = 7
   },
   gender = -- put in here if you have more genders (sex) in your server.
   {
     [0] = "female",
     [1] = "male"
   }
}
if the account manager is disabled.
12.png

if "kickWithSameIP = true" and we try to login a few account managers with the same ip
13.png

I'll work tomorrow on the rest on my todo list, so I can do a final stage and let players enter a server to test the account manager to it's fullest potential and look for flaws :)
 
Haven't looked at the code, but you should make sure you don't copy depot items.
I believe that was an exploit in a previous ingame aac. People would parcel items to the sample players.
Then the players they made would have a copy those items in their depot.
 
Haven't looked at the code, but you should make sure you don't copy depot items.
I believe that was an exploit in a previous ingame aac. People would parcel items to the sample players.
Then the players they made would have a copy those items in their depot.
Thanks for the info :D
but I'm not using sample characters, I insert them from scratch into the database.
Code:
function createCharacter(player, charName, charVoc, level, gender)
   if not verifyCharacterMask(player:getId()) then
     player:sendTextToClient("There has been an error in verifying the Character details, Character creating has been terminated", "Account Manager", 9, TALKTYPE_CHANNEL_R1)
     clearCharacterMask(player:getId())
     return false
   end
   table.insert(existingCharacterNames, charName)
   local pid = db.storeQuery("SELECT `account_id` FROM `players` WHERE `name` = '".. player:getName() .."'")
   local res = 0
   local exp = level == 1 and 0 or 4200
   local lookType = gender == 0 and 136 or 128
   if pid then
     res = result.getDataInt(pid, "account_id")
     db.query("INSERT INTO `players`(`name`, `group_id`, `account_id`, `level`, `vocation`, `health`, `healthmax`, `experience`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `maglevel`, `mana`, `manamax`, `manaspent`, `soul`, `town_id`, `posx`, `posy`, `posz`, `conditions`, `cap`, `sex`, `lastlogin`, `lastip`, `save`, `skull`, `skulltime`, `lastlogout`, `blessings`, `onlinetime`, `deletion`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`) VALUES ('".. charName .."',1,".. res ..",".. level ..",".. charVoc ..",185,185,".. exp ..",68,76,78,58,".. lookType.. ",0,0,40,40,0,100,".. accountManagerConfig.townID ..",".. accountManagerConfig.startDest.x ..",".. accountManagerConfig.startDest.y ..",".. accountManagerConfig.startDest.z ..",0,435,".. gender ..",0,0,1,0,0,0,0,0,0,0,43200,-1,2520,10,0,10,0,10,0,10,0,10,0,10,0,10,0)")
   end
   result.free(pid)
   return true
end
 
Last edited:
Thanks for the info :D
but I'm not using sample characters, I insert them from scratch into the database.

Ah okay :)
Btw, shouldn't the player insert be inside of the if statement. If the first query fails, it'll still try to add the player.
 
Ah okay :)
Btw, shouldn't the player insert be inside of the if statement. If the first query fails, it'll still try to add the player.
You certainly are right but in this case I could even remove the "if pid then" since the account has to exist since a player (who uses the character creation atm) is logged in so the query could not return false (only if there is something completly screwed up) :p
but for safety I'll let it stay and move it inside the if statement, thanks for pointing it out :)
 
Last edited:
what amount of source changes is required to actually run that?
keep in mind that tfs gets changes in sources very often so your code may not work 5 commits later
 
Here are all the changes needed.

events.cpp:

search for:
Code:
playerOnGainSkillTries = -1;
add this after
Code:
playerOnParsePacket = -1;

search for:
Code:
else if (methodName == "onGainSkillTries") {
           playerOnGainSkillTries = event;
         }
add this:
Code:
else if (methodName == "onParsePacket") {
           playerOnParsePacket = event;
         }

go to the bottom of the file and add this:
Code:
bool Events::eventPlayerOnParsePacket(Player* player, uint8_t packet)
{
   // Player:onParsePacket(packet)
   if (playerOnParsePacket == -1) {
     return true;
   }

   if (!scriptInterface.reserveScriptEnv()) {
     std::cout << "[Error - Events::eventPlayerOnParsePacket] Call stack overflow" << std::endl;
     return false;
   }

   ScriptEnvironment* env = scriptInterface.getScriptEnv();
   env->setScriptId(playerOnParsePacket, &scriptInterface);

   lua_State* L = scriptInterface.getLuaState();
   scriptInterface.pushFunction(playerOnParsePacket);

   LuaScriptInterface::pushUserdata<Player>(L, player);
   LuaScriptInterface::setMetatable(L, -1, "Player");

   lua_pushnumber(L, packet);

   return scriptInterface.callFunction(2);
}

events.h

search for:
Code:
void eventPlayerOnGainSkillTries(Player* player, skills_t skill, uint64_t& tries);
add this after:
Code:
bool eventPlayerOnParsePacket(Player* player, uint8_t packet);

search for:
Code:
int32_t playerOnGainSkillTries;
add this:
Code:
int32_t playerOnParsePacket;

game.cpp

search this function:
Code:
bool Game::internalCreatureSay(Creature* creature, SpeakClasses type, const std::string& text,
  bool ghostMode, SpectatorVec* listPtr/* = nullptr*/, const Position* pos/* = nullptr*/)
search inside the function:
Code:
if (text.empty()) {
     return false;
   }
add this after:
Code:
if (creature->getName() == "Account Manager") {
     return false;
   }

iologindata.cpp

search for:
Code:
bool IOLoginData::updateOnlineStatus(uint32_t guid, bool login)
add this straight at the top inside this function:
Code:
std::ostringstream accmanager;
   Database* db = Database::getInstance();
   accmanager << "SELECT `id` FROM `players` WHERE `name` = 'Account Manager'";
   DBResult_ptr result = db->storeQuery(accmanager.str());
   if (!result) {
     return false;
   }

   if ((uint32_t)result->getDataInt("id") == guid) {
     return true;
   }

luascript.cpp

search for:
Code:
//sendGuildChannelMessage(guildId, type, message)
   lua_register(m_luaState, "sendGuildChannelMessage", LuaScriptInterface::luaSendGuildChannelMessage);
add this after:
Code:
//transformToSha1(string)
   lua_register(m_luaState, "transformToSha1", LuaScriptInterface::luaTransformToSha1);

search for:
Code:
registerMethod("Player", "getContainerIndex", LuaScriptInterface::luaPlayerGetContainerIndex);
add this after:
Code:
registerMethod("Player", "disconnectWithReason", LuaScriptInterface::luaPlayerDisconnectWithReason);

search for:
Code:
int32_t LuaScriptInterface::luaSendGuildChannelMessage(lua_State* L)
add this after:
Code:
int32_t LuaScriptInterface::luaTransformToSha1(lua_State* L)
{
   //transformToSha1(string)
   std::string convert = getString(L, 1);
   convert = transformToSHA1(convert);
   pushString(L, convert);
   return 1;
}

search for:
Code:
int32_t LuaScriptInterface::luaPlayerGetContainerIndex(lua_State* L)
add this after:
Code:
int32_t LuaScriptInterface::luaPlayerDisconnectWithReason(lua_State* L)
{
   // player:disconnectWithReason(reason)
   Player* player = getUserdata<Player>(L, 1);
   if (player) {
     player->client->disconnectClient(getString(L, 2));
     player->kickPlayer(true);
     pushBoolean(L, true);
   }
   else {
     lua_pushnil(L);
   }
   return 1;
}

luascript.h

search for:
Code:
static int32_t luaDoSetCreatureLight(lua_State* L);
add this after:
Code:
static int32_t luaTransformToSha1(lua_State* L);

search for:
Code:
static int32_t luaPlayerGetContainerIndex(lua_State* L);
add this after:
Code:
static int32_t luaPlayerDisconnectWithReason(lua_State* L);

protocolgame.cpp

search for:
Code:
#include "scheduler.h"
add this after:
Code:
#include "events.h"

search for:
Code:
extern Chat* g_chat;
add this after:
Code:
extern Events* g_events;

search for:
Code:
if (!_player || g_config.getBoolean(ConfigManager::ALLOW_CLONES)) {
change it to:
Code:
if (!_player || g_config.getBoolean(ConfigManager::ALLOW_CLONES) || name == "Account Manager") {

search for:
Code:
if (g_config.getBoolean(ConfigManager::ONE_PLAYER_ON_ACCOUNT) && player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER && g_game.getPlayerByAccount(player->getAccount())) {
change it to:
Code:
if (g_config.getBoolean(ConfigManager::ONE_PLAYER_ON_ACCOUNT) && name != "Account Manager" && player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER && g_game.getPlayerByAccount(player->getAccount())) {

search for:
Code:
if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) {
     dispatchDisconnectClient("Only clients with protocol " CLIENT_VERSION_STR " allowed!");
     return;
   }
add this after:
Code:
if (accountName.empty() && password.empty()) {
     accountName = "1";
     password = "1";
   }

search for:
Code:
void ProtocolGame::parsePacket(NetworkMessage& msg)
search inside it:
Code:
if (!player) {
     if (recvbyte == 0x0F) {
       disconnect();
     }

     return;
   }
add this after:
Code:
if (!g_events->eventPlayerOnParsePacket(player, recvbyte)) {
     return;
   }

protocolgame.h:

search for:
Code:
void disconnectClient(const std::string& message);
move it from private: to public: (copy the line and erase it, then paste it right above private: )

protocollogin.cpp:

search for:
Code:
void ProtocolLogin::onRecvFirstMessage(NetworkMessage& msg)
search inside this function for:
Code:
if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) {
     dispatchDisconnectClient("Only clients with protocol " CLIENT_VERSION_STR " allowed!");
     return;
   }
add this after:
Code:
if (accountName.empty() && password.empty()) {
     accountName = "1";
     password = "1";
   }

schema.sql: (in your data folder for the database)

search for:
Code:
CREATE TABLE IF NOT EXISTS `accounts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `password` char(40) NOT NULL,
  `type` int(11) NOT NULL DEFAULT '1',
  `premdays` int(11) NOT NULL DEFAULT '0',
  `lastday` int(10) unsigned NOT NULL DEFAULT '0',
  `email` varchar(255) NOT NULL DEFAULT '',
  `creation` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB;
replace it with:
Code:
CREATE TABLE IF NOT EXISTS `accounts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `password` char(40) NOT NULL,
  `type` int(11) NOT NULL DEFAULT '1',
  `premdays` int(11) NOT NULL DEFAULT '0',
  `lastday` int(10) unsigned NOT NULL DEFAULT '0',
  `email` varchar(255) NOT NULL DEFAULT '',
  `creation` int(11) NOT NULL DEFAULT '0',
  `recovery_key` varchar(15) NOT NULL DEFAULT '',
  `pin_code` char(40) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB;

search for:
Code:
CREATE TABLE IF NOT EXISTS `tile_store` (
  `house_id` int(11) NOT NULL,
  `data` longblob NOT NULL,
  FOREIGN KEY (`house_id`) REFERENCES `houses` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
add this after:
Code:
CREATE TABLE IF NOT EXISTS `recovery_key` (
  `id` tinyint(1) NOT NULL,
  `key1` varchar(1) NOT NULL,
  `key2` varchar(1) NOT NULL,
  `key3` varchar(1) NOT NULL,
  `key4` varchar(1) NOT NULL,
  `key5` varchar(1) NOT NULL,
  `key6` varchar(1) NOT NULL,
  `key7` varchar(1) NOT NULL,
  `key8` varchar(1) NOT NULL,
  `key9` varchar(1) NOT NULL,
  UNIQUE KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO `accounts`(`name`, `password`, `type`, `premdays`, `lastday`, `email`, `creation`) VALUES (1, "356a192b7913b04c54574d18c28d46e6395428ab", 0, 0, 0, "", 0);
INSERT INTO `players`(`name`, `group_id`, `account_id`, `level`, `vocation`, `health`, `healthmax`, `experience`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `maglevel`, `mana`, `manamax`, `manaspent`, `soul`, `town_id`, `posx`, `posy`, `posz`, `conditions`, `cap`, `sex`, `lastlogin`, `lastip`, `save`, `skull`, `skulltime`, `lastlogout`, `blessings`, `onlinetime`, `deletion`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`) VALUES ('Account Manager',4,1,1,0,1,1,0,68,76,78,58,231,0,0,1,1,0,100,1,880,1440,7,0,435,1,0,0,0,0,0,0,0,0,0,0,43200,-1,2520,10,0,10,0,10,0,10,0,10,0,10,0,10,0);

Those are the changes which need to be done, I don't plan on changing more in the core, since I can do the rest just fine in lua.
 
Last edited:
I currently have a live server where you guys can test it, I would really much appreciate all feedback I can get, as I want to make it as easy useable as I can :)
Client 10.41
ip: 84.179.114.214
port: 7171

Just leave everything blank and login (no 1/1 required)
 
I currently have a live server where you guys can test it, I would really much appreciate all feedback I can get, as I want to make it as easy useable as I can :)
Client 10.41
ip: 84.179.114.214
port: 7171

Just leave everything blank and login (no 1/1 required)

Tried it out. Just a couple things, right now I couldn't make a character with two names, and a space between, had to be one word or it contained illegal characters it said. You should notice my characters name was onename or something like that, I was just trying different names since it wouldn't allow the ones I tried. Also once you are done it shows you the recovery key, but doesn't tell you that it's done and you can relog with the account. Otherwise it is very nice, major props!

Edit: Forgot, don't know if you meant it like this or not, but in the default channel a player can still perform some talkactions such as !online, or something like that...
 
Tried it out. Just a couple things, right now I couldn't make a character with two names, and a space between, had to be one word or it contained illegal characters it said. You should notice my characters name was onename or something like that, I was just trying different names since it wouldn't allow the ones I tried. Also once you are done it shows you the recovery key, but doesn't tell you that it's done and you can relog with the account. Otherwise it is very nice, major props!

Edit: Forgot, don't know if you meant it like this or not, but in the default channel a player can still perform some talkactions such as !online, or something like that...
I'm currently working on a better filter for character names so spaces will be allowed.
I'll add a message once the account is done to make the player aware that he can login to his character :)
Glad you told me about the talkactions, wasn't aware of that fact, need to disable that for the account manager ofc.

Thanks for the feedback will sit myself behind that in a few :D
 
I'm currently working on a better filter for character names so spaces will be allowed.
I'll add a message once the account is done to make the player aware that he can login to his character :)
Glad you told me about the talkactions, wasn't aware of that fact, need to disable that for the account manager ofc.

Thanks for the feedback will sit myself behind that in a few :D
No problem, had a few minutes and was interested in how it looked anyways. Besides you asked for feedback and no one had said anything so I figured it would help you out some.
 
No problem, had a few minutes and was interested in how it looked anyways. Besides you asked for feedback and no one had said anything so I figured it would help you out some.
I've finished fixing all the stuff you mentioned, if you are interested into taking a second look :)
I'm also using a filter for names now and spaces are ofc allowed now aswell.
Code:
blockedNames = {"GM","GOD","CM","Community Manager","Tutor"}, -- insert all patterns in here for names which you find not appropriate.

I've just stumbled into an issue with the change password, problem is that the client ofc wont let me insert the old password to check, so I have to think about another solution for that case :p
 
I've finished fixing all the stuff you mentioned, if you are interested into taking a second look :)
I'm also using a filter for names now and spaces are ofc allowed now aswell.
Code:
blockedNames = {"GM","GOD","CM","Community Manager","Tutor"}, -- insert all patterns in here for names which you find not appropriate.

I've just stumbled into an issue with the change password, problem is that the client ofc wont let me insert the old password to check, so I have to think about another solution for that case :p

Looks very nice. You will see a 'Three Word Name' character in your db. Was great job! No longer able to do the talkactions, and it let me know what I needed to know

20:38 Account Manager: Your Account has been created, you can login now.

Great work man! If I could think of anything else I would change or add to it, I will let you know.
 
I've taken @Flatlander idea into consideration about the pin code and implemented it, as there was a problem with the client overall which ofc wouldn't let us enter our passwords into chat, so I couldn't use password as a reference to change account stuff in the manager once you are logged in.

looks like this now:
14.png

The pin code will be saved in the database with a sha1 encryption.
With this last puzzle I can finally finish everything around it and provide a beta version for live implementation on servers.
 
Here are the 2 last remaining things which where on my TODO list.

Change Password:
15.png

Change Email:
16.png


As of now I'm searching up to 5 server owners (tfs 1.x) who want to try it out live on their server, I help you implement the entire system step by step or give you a detailed guide on how to.
either leave a message here or pm me
 
Very nice work! The pin code was a great idea @Flatlander

Edit: Second post showed up after I posted.

I personally don't have a need for the account manager, and my personal preference is to have players use the website, however my opinion isn't the one that matters, it's the players. Seeing as how players like these systems, and you need some volunteers to try it out, I will volunteer. You can private message me with the organized code, I don't know how complicated it is, but I can probably just implement it on my own if you prefer to just zip the files. Anyways, I just got home from school and got a few things on my agenda, but I'll be back to my computer in less than a half hour, and I shall implement this system into myserver for testing if you want me to. Besides, I'd still like to get my hands on a copy for the players sake.
 
It was Xagulz original idea. I just liked it when I saw it. (You can see the pin code being used on Deathzot)
 
Back
Top