Jump to content

PIN code system


Lemongrass

Recommended Posts


  • Group:  Developer
  • Topic Count:  28
  • Topics Per Day:  0.01
  • Content Count:  547
  • Reputation:   270
  • Joined:  11/08/11
  • Last Seen:  

Howdy guys,

 

today I worked on something odd and nerving. The pincode system. I got help from Yommy for some of the packets and the decrypt function.

But basically I want someone to review my code and additionally I would need some further information about the system.

 

Required infos:

  • How often does a user on kRO have to change his PIN code?
  • The PIN code system checks for the KSSN on AEGIS, I would say we leave that out or do a birthday check instead. Which would mess up the client translation, because that one says "You cant use your KSSN".
  • How can a user delete his PIN?
  • When is the button for the pincode system displayed in char select? Send state 7 to find it there.
  • Has anybody got an UI translation for that system?
  • How often can a user enter his PIN code wrong and what happens then?

Thanks in advance and have fun testing.

 

 

Index: conf/char_athena.conf
===================================================================
--- conf/char_athena.conf    (revision 17179)
+++ conf/char_athena.conf    (working copy)
@@ -159,4 +159,16 @@
 // What folder the DB files are in (item_db.txt, etc.)
 db_path: db
 
+// Pincode system
+// A window is opened before you can select your character and you will have to enter a pincode by using only your mouse
+// 0: disabled
+// 1: enabled
+pincode_enabled: 0
+
+// How often does a user have to change his pincode?
+// Default: 1209600 (2 weeks)
+// 0: never
+// X: every X seconds
+pincode_changetime: 1209600
+
 import: conf/import/char_conf.txt
Index: sql-files/upgrades/upgrade_svn17179.sql
===================================================================
--- sql-files/upgrades/upgrade_svn17179.sql    (revision 0)
+++ sql-files/upgrades/upgrade_svn17179.sql    (working copy)
@@ -0,0 +1,2 @@
+ALTER TABLE `login` ADD COLUMN `pincode` varchar(4) NOT NULL DEFAULT '';
+ALTER TABLE `login` ADD COLUMN `pincode_change` int(11) unsigned NOT NULL DEFAULT '0';
\ No newline at end of file
Index: src/char/char.c
===================================================================
--- src/char/char.c    (revision 17179)
+++ src/char/char.c    (working copy)
@@ -131,6 +131,9 @@
     uint8 clienttype;
     char new_name[NAME_LENGTH];
     char birthdate[10+1];  // YYYY-MM-DD
+    char pincode[4+1];
+    uint16 pincode_seed;
+    time_t pincode_change;
 };
 
 int max_connect_user = -1;
@@ -141,6 +144,23 @@
 int start_armor = 2301;
 int guild_exp_rate = 100;
 
+// Pincode system
+#define PINCODE_OK 0
+#define PINCODE_ASK 1
+#define PINCODE_NOTSET 2
+#define PINCODE_EXPIRED 3
+#define    PINCODE_WRONG 8
+
+int pincode_enabled = PINCODE_OK; // PINCODE_OK = off, PINCODE_ASK = on
+int pincode_changetime =  14 * 24 * 60 * 60; // every 14 days
+
+void pincode_check( int fd, struct char_session_data* sd );
+void pincode_change( int fd, struct char_session_data* sd );
+void pincode_setnew( int fd, struct char_session_data* sd );
+void pincode_sendstate( int fd, struct char_session_data* sd, uint16 state );
+void pincode_notifylogin( int account_id, char* pin );
+void pincode_decrypt( unsigned long userSeed, char* pin );
+
 //Custom limits for the fame lists. [Skotlex]
 int fame_list_size_chemist = MAX_FAME_LIST;
 int fame_list_size_smith = MAX_FAME_LIST;
@@ -2147,7 +2167,7 @@
         break;
 
         case 0x2717: // account data
-            if (RFIFOREST(fd) < 63)
+            if (RFIFOREST(fd) < 68)
                 return 0;
 
             // find the authenticated session with this account id
@@ -2165,6 +2185,7 @@
                 } else if ( !sd->char_slots )/* no value aka 0 in sql */
                     sd->char_slots = MAX_CHARS;/* cap to maximum */
                 safestrncpy(sd->birthdate, (const char*)RFIFOP(fd,52), sizeof(sd->birthdate));
+                safestrncpy(sd->pincode, (const char*)RFIFOP(fd,63), sizeof(sd->pincode));
                 ARR_FIND( 0, ARRAYLENGTH(server), server_id, server[server_id].fd > 0 && server[server_id].map[0] );
                 // continued from char_auth_ok...
                 if( server_id == ARRAYLENGTH(server) || //server not online, bugreport:2359
@@ -2179,18 +2200,28 @@
                     // send characters to player
                     mmo_char_send006b(i, sd);
 #if PACKETVER >=  20110309
-                    // PIN code system, disabled
-                    WFIFOHEAD(i, 12);
-                    WFIFOW(i, 0) = 0x08B9;
-                    WFIFOW(i, 2) = 0;
-                    WFIFOW(i, 4) = 0;
-                    WFIFOL(i, 6) = sd->account_id;
-                    WFIFOW(i, 10) = 0;
-                    WFIFOSET(i, 12);
+                    if( pincode_enabled ){
+                        // PIN code system enabled
+                        if( strlen( sd->pincode ) <= 0 ){
+                            // No PIN code has been set yet
+                            pincode_sendstate( i, sd, PINCODE_NOTSET );
+                        }else{
+                            if( !pincode_changetime && sd->pincode_change > time(NULL) ){
+                                // Ask user for his PIN code
+                                pincode_sendstate( i, sd, PINCODE_ASK );
+                            }else{
+                                // User hasnt changed his PIN code too long
+                                pincode_sendstate( i, sd, PINCODE_EXPIRED );
+                            }
+                        }
+                    }else{
+                        // PIN code system, disabled
+                        pincode_sendstate( i, sd, PINCODE_OK );
+                    }
 #endif
                 }
             }
-            RFIFOSKIP(fd,63);
+            RFIFOSKIP(fd,68);
         break;
 
         // login-server alive packet
@@ -4188,6 +4219,50 @@
         }
         return 0; // avoid processing of followup packets here
 
+        // checks the entered pin
+        case 0x8b8:
+            pincode_check( fd, sd );
+        break;
+
+        // request for security window?
+        case 0x8c5:
+            if( RFIFOREST(fd) < 6 )
+                return 0;
+
+            if( RFIFOL(fd,2) != sd->account_id )
+                break;
+
+            // Obviously we should do something here. Open up pincode dialog?
+            pincode_sendstate( fd, sd, 0 );
+
+            RFIFOSKIP(fd,6);
+        break;
+
+        // pincode change request
+        case 0x8be:
+            if( RFIFOREST(fd) < 14 )
+                return 0;
+
+            if( RFIFOL(fd,2) != sd->account_id )
+                break;
+
+            pincode_change( fd, sd );
+
+            RFIFOSKIP(fd,14);
+        break;
+
+        case 0x8ba:
+            if( RFIFOREST(fd) < 10 )
+                return 0;
+
+            if( RFIFOL(fd,2) != sd->account_id )
+                break;
+
+            pincode_setnew( fd, sd );
+
+            RFIFOSKIP(fd,10);
+        break;
+
         // unknown packet received
         default:
             ShowError("parse_char: Received unknown packet "CL_WHITE"0x%x"CL_RESET" from ip '"CL_WHITE"%s"CL_RESET"'! Disconnecting!\n", RFIFOW(fd,0), ip2str(ipl, NULL));
@@ -4369,6 +4444,102 @@
 }
 
 //------------------------------------------------
+//Pincode system
+//------------------------------------------------
+void pincode_check( int fd, struct char_session_data* sd ){
+    char pin[5] = "\0\0\0\0";
+    strncpy((char*)pin, (char*)RFIFOP(fd, 6), 4+1);
+
+    pincode_decrypt(sd->pincode_seed, pin );
+
+    if( strcmp( sd->pincode, pin ) == 0 ){
+        pincode_sendstate( fd, sd, PINCODE_OK );
+    }else{
+        pincode_sendstate( fd, sd, PINCODE_WRONG );
+    }
+}
+
+void pincode_change( int fd, struct char_session_data* sd ){
+    char oldpin[5] = "\0\0\0\0";
+    char newpin[5] = "\0\0\0\0";
+
+    strncpy(oldpin, (char*)RFIFOP(fd,6), 4+1);
+    pincode_decrypt(sd->pincode_seed,oldpin);
+
+    if( strcmp( sd->pincode, oldpin ) != 0 ){
+        pincode_sendstate( fd, sd, PINCODE_WRONG );
+        return;
+    }
+
+    strncpy(newpin, (char*)RFIFOP(fd,10), 4+1);
+    pincode_decrypt(sd->pincode_seed,newpin);
+
+    pincode_notifylogin( sd->account_id, newpin );
+
+    pincode_sendstate( fd, sd, PINCODE_OK );
+}
+
+void pincode_setnew( int fd, struct char_session_data* sd ){
+    char newpin[5] = "\0\0\0\0";
+
+    strncpy(newpin, (char*)RFIFOP(fd,6), 4+1);
+    pincode_decrypt(sd->pincode_seed,newpin);
+
+    pincode_notifylogin( sd->account_id, newpin );
+
+    pincode_sendstate( fd, sd, PINCODE_OK );
+}
+
+// 0 = disabled / pin is correct
+// 1 = ask for pin - client sends 0x8b8
+// 2 = create new pin - client sends 0x8ba
+// 3 = pin must be changed - client 0x8be
+// 4 = create new pin ?? - client sends 0x8ba
+// 5 = client shows msgstr(1896)
+// 6 = client shows msgstr(1897) Unable to use your KSSN number
+// 7 = char select window shows a button - client sends 0x8c5
+// 8 = pincode was incorrect
+void pincode_sendstate( int fd, struct char_session_data* sd, uint16 state ){
+    WFIFOHEAD(fd, 12);
+    WFIFOW(fd, 0) = 0x8b9;
+    WFIFOL(fd, 2) = sd->pincode_seed = rand() % 0xFFFF;
+    WFIFOL(fd, 6) = sd->account_id;
+    WFIFOW(fd,10) = state;
+    WFIFOSET(fd,12);
+}
+
+void pincode_notifylogin( int account_id, char* pin ){
+    WFIFOHEAD(login_fd,15);
+    WFIFOW(login_fd,0) = 0x2738;
+    WFIFOL(login_fd,2) = account_id;
+    strncpy( (char*)WFIFOP(login_fd,6), pin, 5 );
+    WFIFOL(login_fd,11) = pincode_changetime;
+    WFIFOSET(login_fd,15);
+}
+
+void pincode_decrypt( unsigned long userSeed, char* pin ){
+    int i, pos;
+    char tab[10] = {0,1,2,3,4,5,6,7,8,9};
+    unsigned long multiplier = 0x3498, baseSeed = 0x881234;
+
+    for( i = 1; i < 10; i++ ){
+        userSeed = baseSeed + userSeed * multiplier;
+        pos = userSeed % (i + 1);
+        if( i != pos ){
+            tab[i] ^= tab[pos];
+            tab[pos] ^= tab[i];
+            tab[i] ^= tab[pos];
+        }
+    }
+    
+    for( i = 0; i < 4; i++ ){
+        pin[i] = tab[pin[i]- '0'];
+    }
+
+    sprintf(pin, "%d%d%d%d", pin[0], pin[1], pin[2], pin[3]);
+}
+
+//------------------------------------------------
 //Invoked 15 seconds after mapif_disconnectplayer in case the map server doesn't
 //replies/disconnect the player we tried to kick. [Skotlex]
 //------------------------------------------------
@@ -4691,6 +4862,10 @@
             }
         } else if (strcmpi(w1, "guild_exp_rate") == 0) {
             guild_exp_rate = atoi(w2);
+        } else if (strcmpi(w1, "pincode_enabled") == 0) {
+            pincode_enabled = atoi(w2);
+        } else if (strcmpi(w1, "pincode_changetime") == 0) {
+            pincode_changetime = atoi(w2);
         } else if (strcmpi(w1, "import") == 0) {
             char_config_read(w2);
         }
Index: src/char/inter.c
===================================================================
--- src/char/inter.c    (revision 17179)
+++ src/char/inter.c    (working copy)
@@ -387,7 +387,7 @@
     account_id = atoi(query);
 
     if (account_id < START_ACCOUNT_NUM) {    // is string
-        if ( SQL_ERROR == Sql_Query(sql_handle, "SELECT `account_id`,`name`,`class`,`base_level`,`job_level`,`online` FROM `char` WHERE `name` LIKE '%s' LIMIT 10", query_esq)
+        if ( SQL_ERROR == Sql_Query(sql_handle, "SELECT `account_id`,`name`,`class`,`base_level`,`job_level`,`online` FROM `%s` WHERE `name` LIKE '%s' LIMIT 10", char_db, query_esq)
                 || Sql_NumRows(sql_handle) == 0 ) {
             if( Sql_NumRows(sql_handle) == 0 ) {
                 inter_to_fd(fd, u_fd, aid, "No matches were found for your criteria, '%s'",query);
Index: src/login/account.h
===================================================================
--- src/login/account.h    (revision 17179)
+++ src/login/account.h    (working copy)
@@ -50,6 +50,8 @@
     char lastlogin[24];     // date+time of last successful login
     char last_ip[16];       // save of last IP of connection
     char birthdate[10+1];   // assigned birth date (format: YYYY-MM-DD, default: 0000-00-00)
+    char pincode[4+1];        // pincode system
+    time_t pincode_change;    // (timestamp): last time of pincode change
     int account_reg2_num;
     struct global_reg account_reg2[ACCOUNT_REG2_NUM]; // account script variables (stored on login server)
 };
Index: src/login/account_sql.c
===================================================================
--- src/login/account_sql.c    (revision 17179)
+++ src/login/account_sql.c    (working copy)
@@ -522,7 +522,7 @@
 
     // retrieve login entry for the specified account
     if( SQL_ERROR == Sql_Query(sql_handle,
-        "SELECT `account_id`,`userid`,`user_pass`,`sex`,`email`,`group_id`,`state`,`unban_time`,`expiration_time`,`logincount`,`lastlogin`,`last_ip`,`birthdate`,`character_slots` FROM `%s` WHERE `account_id` = %d",
+        "SELECT `account_id`,`userid`,`user_pass`,`sex`,`email`,`group_id`,`state`,`unban_time`,`expiration_time`,`logincount`,`lastlogin`,`last_ip`,`birthdate`,`character_slots`,`pincode`, `pincode_change` FROM `%s` WHERE `account_id` = %d",
         db->account_db, account_id )
     ) {
         Sql_ShowDebug(sql_handle);
@@ -549,6 +549,8 @@
     Sql_GetData(sql_handle, 11, &data, NULL); safestrncpy(acc->last_ip, data, sizeof(acc->last_ip));
     Sql_GetData(sql_handle, 12, &data, NULL); safestrncpy(acc->birthdate, data, sizeof(acc->birthdate));
     Sql_GetData(sql_handle, 13, &data, NULL); acc->char_slots = atoi(data);
+    Sql_GetData(sql_handle, 14, &data, NULL); safestrncpy(acc->pincode, data, sizeof(acc->pincode));
+    Sql_GetData(sql_handle, 15, &data, NULL); acc->pincode_change = atol(data);
 
     Sql_FreeResult(sql_handle);
 
@@ -597,7 +599,7 @@
     if( is_new )
     {// insert into account table
         if( SQL_SUCCESS != SqlStmt_Prepare(stmt,
-            "INSERT INTO `%s` (`account_id`, `userid`, `user_pass`, `sex`, `email`, `group_id`, `state`, `unban_time`, `expiration_time`, `logincount`, `lastlogin`, `last_ip`, `birthdate`, `character_slots`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+            "INSERT INTO `%s` (`account_id`, `userid`, `user_pass`, `sex`, `email`, `group_id`, `state`, `unban_time`, `expiration_time`, `logincount`, `lastlogin`, `last_ip`, `birthdate`, `character_slots`, `pincode`, `pincode_change`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
             db->account_db)
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt,  0, SQLDT_INT,    (void*)&acc->account_id,      sizeof(acc->account_id))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt,  1, SQLDT_STRING, (void*)acc->userid,           strlen(acc->userid))
@@ -613,6 +615,8 @@
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 11, SQLDT_STRING, (void*)&acc->last_ip,         strlen(acc->last_ip))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 12, SQLDT_STRING, (void*)&acc->birthdate,       strlen(acc->birthdate))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 13, SQLDT_UCHAR,  (void*)&acc->char_slots,      sizeof(acc->char_slots))
+        ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 14, SQLDT_STRING, (void*)&acc->pincode,           sizeof(acc->pincode))
+        ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 15, SQLDT_LONG,    (void*)&acc->pincode_change, sizeof(acc->pincode_change))
         ||  SQL_SUCCESS != SqlStmt_Execute(stmt)
         ) {
             SqlStmt_ShowDebug(stmt);
@@ -621,7 +625,7 @@
     }
     else
     {// update account table
-        if( SQL_SUCCESS != SqlStmt_Prepare(stmt, "UPDATE `%s` SET `userid`=?,`user_pass`=?,`sex`=?,`email`=?,`group_id`=?,`state`=?,`unban_time`=?,`expiration_time`=?,`logincount`=?,`lastlogin`=?,`last_ip`=?,`birthdate`=?,`character_slots`=? WHERE `account_id` = '%d'", db->account_db, acc->account_id)
+        if( SQL_SUCCESS != SqlStmt_Prepare(stmt, "UPDATE `%s` SET `userid`=?,`user_pass`=?,`sex`=?,`email`=?,`group_id`=?,`state`=?,`unban_time`=?,`expiration_time`=?,`logincount`=?,`lastlogin`=?,`last_ip`=?,`birthdate`=?,`character_slots`=?,`pincode`=?, `pincode_change`=? WHERE `account_id` = '%d'", db->account_db, acc->account_id)
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt,  0, SQLDT_STRING, (void*)acc->userid,           strlen(acc->userid))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt,  1, SQLDT_STRING, (void*)acc->pass,             strlen(acc->pass))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt,  2, SQLDT_ENUM,   (void*)&acc->sex,             sizeof(acc->sex))
@@ -635,6 +639,8 @@
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 10, SQLDT_STRING, (void*)&acc->last_ip,         strlen(acc->last_ip))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 11, SQLDT_STRING, (void*)&acc->birthdate,       strlen(acc->birthdate))
         ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 12, SQLDT_UCHAR,  (void*)&acc->char_slots,      sizeof(acc->char_slots))
+        ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 13, SQLDT_STRING, (void*)&acc->pincode,           strlen(acc->pincode))
+        ||  SQL_SUCCESS != SqlStmt_BindParam(stmt, 14, SQLDT_LONG,   (void*)&acc->pincode_change,  sizeof(acc->pincode_change))
         ||  SQL_SUCCESS != SqlStmt_Execute(stmt)
         ) {
             SqlStmt_ShowDebug(stmt);
Index: src/login/login.c
===================================================================
--- src/login/login.c    (revision 17179)
+++ src/login/login.c    (working copy)
@@ -562,6 +562,7 @@
             uint8 char_slots = 0;
             int group_id = 0;
             char birthdate[10+1] = "";
+            char pincode[4+1] = "";
 
             int account_id = RFIFOL(fd,2);
             RFIFOSKIP(fd,6);
@@ -574,9 +575,11 @@
                 group_id = acc.group_id;
                 char_slots = acc.char_slots;
                 safestrncpy(birthdate, acc.birthdate, sizeof(birthdate));
+                safestrncpy(pincode, acc.pincode, sizeof(pincode));
+                acc.pincode_change = 0;
             }
 
-            WFIFOHEAD(fd,63);
+            WFIFOHEAD(fd,68);
             WFIFOW(fd,0) = 0x2717;
             WFIFOL(fd,2) = account_id;
             safestrncpy((char*)WFIFOP(fd,6), email, 40);
@@ -584,7 +587,8 @@
             WFIFOB(fd,50) = (unsigned char)group_id;
             WFIFOB(fd,51) = char_slots;
             safestrncpy((char*)WFIFOP(fd,52), birthdate, 10+1);
-            WFIFOSET(fd,63);
+            safestrncpy((char*)WFIFOP(fd,63), pincode, 4+1 );
+            WFIFOSET(fd,68);
         }
         break;
 
@@ -910,6 +914,27 @@
             RFIFOSKIP(fd,2);
         break;
 
+        case 0x2738: //Change PIN Code for a account
+            if( RFIFOREST(fd) < 15 )
+                return 0;
+
+        {
+            struct mmo_account acc;
+
+            if( accounts->load_num(accounts, &acc, RFIFOL(fd,2) ) )
+            {
+                strncpy( acc.pincode, (char*)RFIFOP(fd,6), 5 );
+                acc.pincode_change = RFIFOL(fd,11);
+                if( acc.pincode_change > 0 ){
+                    acc.pincode_change += time( NULL );
+                }
+                accounts->save(accounts, &acc);
+            }
+
+            RFIFOSKIP(fd,15);
+        }
+        break;
+
         default:
             ShowError("parse_fromchar: Unknown packet 0x%x from a char-server! Disconnecting!\n", command);
             set_eof(fd);
@@ -961,6 +986,8 @@
     safestrncpy(acc.lastlogin, "0000-00-00 00:00:00", sizeof(acc.lastlogin));
     safestrncpy(acc.last_ip, last_ip, sizeof(acc.last_ip));
     safestrncpy(acc.birthdate, "0000-00-00", sizeof(acc.birthdate));
+    safestrncpy(acc.pincode, "", sizeof(acc.pincode));
+    acc.pincode_change = 0;
 
     acc.char_slots = 0;

pincode_system.patch

  • Upvote 3
Link to comment
Share on other sites


  • Group:  Members
  • Topic Count:  20
  • Topics Per Day:  0.00
  • Content Count:  398
  • Reputation:   140
  • Joined:  01/04/12
  • Last Seen:  

  • How often does a user on kRO have to change his PIN code? - You don't have to change it ever if you don't want.
  • The PIN code system checks for the KSSN on AEGIS, I would say we leave that out or do a birthday check instead. Which would mess up the client translation, because that one says "You cant use your KSSN". - Leaving it out for now is fine, I guess. Other opinions?
  • How can a user delete his PIN? - You cannot delete your PIN, you MUST use it when enabled.
  • When is the button for the pincode system displayed in char select? Send state 7 to find it there. - Right after getting to char select
  • Has anybody got an UI translation for that system? - This are the translations, I can come up with all the buttons - if needed.
  • How often can a user enter his PIN code wrong and what happens then? - 3x, nothing really, you get kicked back to the login screen and have to log back in to char select.

Tf.png

  • Upvote 2
Link to comment
Share on other sites


  • Group:  Developer
  • Topic Count:  28
  • Topics Per Day:  0.01
  • Content Count:  547
  • Reputation:   270
  • Joined:  11/08/11
  • Last Seen:  

Thats strange then because there are packets for deleting and a must change packet.

The problem with sending state 7 directly when you come to char select is that you dont have to enter the pin if you dont click that button. I think I know a workarround for this.

So is anyone against commiting this?

Link to comment
Share on other sites


  • Group:  Members
  • Topic Count:  20
  • Topics Per Day:  0.00
  • Content Count:  398
  • Reputation:   140
  • Joined:  01/04/12
  • Last Seen:  

That weird, my PIN is set since sometime in 2011, I guess? And never was forced to change it. As you can see there's no button for deleting it either.

 

I didn't have a change testing yet, haven't been home for 2 days and probably won't be for another 1-2 but if everything is working fine, I'd say go ahead and commit if nobody minds.

 

Good job on this by the way.

Link to comment
Share on other sites


  • Group:  Developer
  • Topic Count:  28
  • Topics Per Day:  0.01
  • Content Count:  547
  • Reputation:   270
  • Joined:  11/08/11
  • Last Seen:  

Commited with some further changes because of Flaid's information in r17183.

  • Upvote 5
Link to comment
Share on other sites

×
×
  • Create New...