commit 62c22f43f84e24ebce98e62233d998aa325fa052 Author: Thyth Date: Sat Feb 7 21:24:47 2015 -0800 Initial release of the TribesNext scripts for Tribes 2. This contains incomplete in-game GUI code for player and clan profile management. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1af8c1b --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +TribesNext +========== + +Scripts for system integration with Tribes 2 +-------------------------------------------- + +For more details, see the TribesNext project at: www.tribesnext.com diff --git a/loginScreens.cs b/loginScreens.cs new file mode 100644 index 0000000..96bcbdb --- /dev/null +++ b/loginScreens.cs @@ -0,0 +1,1139 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Krash & Electricutioner/Thyth +// Copyright 2008-2009 by Electricutioner/Thyth, and the Tribes 2 Community System Reengineering Intitiative + +// Login UIs and Account processing jumble. + +$LastLoginKey = $pref::LastLoginKey; +exec("scripts/commonDialogs.cs"); +exec("gui/MessageBoxDlg.gui"); +exec("t2csri/glue.cs"); + + // Begin UI replacements: + new GuiBitmapCtrl(TN_logo) + { + profile = "GuiDefaultProfile"; + horizSizing = "center"; + vertSizing = "top"; + bitmap = "TN_logo"; + position = "0 20"; + extent = "640 105"; + visible = true; + minExtent = "8 8"; + helpTag = "0"; + }; + new GuiControlProfile(noMoreModal) + { + modal = false; + }; + new GuiControlProfile (ShellTextCenterProfile) + { + fontType = "Univers Condensed"; + fontSize = 18; + fontColor = "66 229 244"; + justify = "center"; + autoSizeWidth = false; + autoSizeHeight = true; + Modal = false; + }; + new GuiControlProfile (ShellTextLeftProfile) + { + fontType = "Univers Condensed"; + fontSize = 18; + fontColor = "66 229 244"; + justify = "left"; + autoSizeWidth = false; + autoSizeHeight = true; + Modal = false; + }; + + new GuiControl(CreateAccountDlg) { + profile = "GuiDefaultProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "0 0"; + extent = "640 480"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + open = "0"; + + new ShellPaneCtrl(TitleBar) { + profile = "ShellDlgPaneProfile"; + horizSizing = "center"; + vertSizing = "center"; + position = "70 36"; + extent = "500 408"; + minExtent = "48 92"; + visible = "1"; + helpTag = "0"; + text = "Create Account - Step 1 of 3"; + noTitleBar = "0"; + + + new GuiControlProfile ("BrowserH1Profile") + { + fontType = "Univers Condensed Bold"; + fontSize = 28; + fontColor = "66 219 234"; + autoSizeWidth = false; + autoSizeHeight = true; + bitmapBase = "gui/shll"; + }; + new GuiBitmapCtrl(tn_EntropyBox) { + profile = "GuiDefaultProfile"; + horizSizing = "center"; + vertSizing = "bottom"; + position = "37 84"; + extent = "440 188"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + + new GuiMouseEventCtrl(tn_EntropyEvent) + { + profile = "DefaultProfile"; + position = "0 0"; + extent = "440 188"; + vertSizing = "top"; + horizSizing = "left"; + visible = "true"; + }; + }; + + new GuiMLTextCtrl(AccountInstructions) { + profile = "BrowserH1Profile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "26 34"; + extent = "390 14"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + lineSpacing = "2"; + }; + new GuiMLTextCtrl(AccountText) { + profile = "ShellMessageTextProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "26 74"; + extent = "445 16"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + lineSpacing = "-2"; + }; + new GuiTextCtrl(CN_keyName) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "35 128"; + extent = "100 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Key Details:"; + }; + new GuiMLTextCtrl(CA_keyName) { + profile = "ShellTextLeftProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "141 128"; + extent = "200 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = ""; + }; + new GuiTextCtrl(CN_userName) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "35 174"; + extent = "100 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Account Name:"; + }; + new GuiTextCtrl(CN_chooPass) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "35 220"; + extent = "100 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Password:"; + }; + new GuiTextCtrl(CN_confPass) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "35 250"; + extent = "100 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Confirm Password:"; + }; + new ShellTextEditCtrl(CA_userName) { + profile = "NewTextEditProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "131 166"; + extent = "180 38"; + minExtent = "32 38"; + visible = "1"; + variable = "$CreateAccountLoginName"; + command = "CA_userName.validateWarriorName();"; + IRCName = true; + helpTag = "0"; + historySize = "0"; + maxLength = "16"; + password = "0"; + glowOffset = "9 9"; + }; + new ShellTextEditCtrl(CA_chooPass) { + profile = "NewTextEditProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "131 212"; + extent = "180 38"; + minExtent = "32 38"; + visible = "1"; + variable = "$CreateAccountPassword"; + helpTag = "0"; + historySize = "0"; + maxLength = "255"; + password = "1"; + glowOffset = "9 9"; + }; + new ShellTextEditCtrl(CA_confPass) { + profile = "NewTextEditProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "131 242"; + extent = "180 38"; + minExtent = "32 38"; + visible = "1"; + variable = "$CreateAccountConfirmPassword"; + helpTag = "0"; + historySize = "0"; + maxLength = "255"; + password = "1"; + glowOffset = "9 9"; + }; + new ShellBitmapButton(CreateAccountPrevBtn) { + profile = "ShellButtonProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "72 351"; + extent = "128 38"; + minExtent = "32 38"; + visible = "1"; + command = "CreateAccountDlg.nextBtn(1);"; + accelerator = "escape"; + helpTag = "0"; + text = "CANCEL"; + simpleStyle = "0"; + }; + new GuiTextCtrl(CN_strength) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "37 288"; + extent = "85 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Strength:"; + maxLength = "255"; + }; + new ShellPopupMenu(CA_strength) { + profile = "ShellPopupProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "118 280"; + extent = "140 38"; + minExtent = "32 38"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + text = "RSA-512"; + helpTag = "0"; + glowOffset = "9 9"; + maxLength = "255"; + longTextBuffer = "0"; + maxPopupHeight = "200"; + buttonBitmap = "gui/shll_pulldown"; + rolloverBarBitmap = "gui/shll_pulldownbar_rol"; + selectedBarBitmap = "gui/shll_pulldownbar_act"; + noButtonStyle = "0"; + }; + new ShellBitmapButton(CreateAccountGenBtn) { + profile = "ShellButtonProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "250 280"; + extent = "189 38"; + minExtent = "32 38"; + visible = "1"; + command = "CreateAccountDlg.genBtn();"; + helpTag = "1"; + text = "GENERATE YOUR KEY"; + simpleStyle = "0"; + }; + new GuiMLTextCtrl(HintText) { + profile = "ShellTextCenterProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "125 255"; + extent = "445 16"; + minExtent = "8 8"; + visible = "1"; + helpTag = "1"; + lineSpacing = "-2"; + }; + new GuiTextCtrl(HintText2) { + profile = "ShellTextCenterProfile"; + horizSizing = "center"; + vertSizing = "bottom"; + position = "0 315"; + extent = "445 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Click the above button to proceed."; + maxLength = "255"; + }; + new ShellBitmapButton(CreateAccountNextBtn) { + profile = "ShellButtonProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "282 351"; + extent = "128 38"; + minExtent = "32 38"; + visible = "1"; + command = "CreateAccountDlg.nextBtn();"; + helpTag = "0"; + text = "NEXT STEP"; + simpleStyle = "0"; + }; + }; + }; + + // Modified Login dlg + new GuiControl(LoginDlg) { + profile = "GuiDefaultProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "0 0"; + extent = "640 480"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + + new ShellPaneCtrl() { + profile = "ShellDlgPaneProfile"; + horizSizing = "center"; + vertSizing = "top"; + position = "72 143"; + extent = "495 194"; + minExtent = "48 92"; + visible = "1"; + helpTag = "0"; + text = "LOGIN"; + maxLength = "255"; + noTitleBar = "0"; + + new GuiTextCtrl(accnTxt) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "37 77"; + extent = "85 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Account:"; + maxLength = "255"; + }; + new ShellPopupMenu(LoginEditMenu) { + profile = "ShellPopupProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "118 69"; + extent = "180 38"; + minExtent = "32 38"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + text = "Select Account"; + helpTag = "0"; + glowOffset = "9 9"; + maxLength = "255"; + longTextBuffer = "0"; + maxPopupHeight = "200"; + buttonBitmap = "gui/shll_pulldown"; + rolloverBarBitmap = "gui/shll_pulldownbar_rol"; + selectedBarBitmap = "gui/shll_pulldownbar_act"; + noButtonStyle = "0"; + }; + new ShellTextEditCtrl(LoginEditBox) { + profile = "NewTextEditProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "118 99"; + extent = "180 38"; + minExtent = "32 38"; + visible = "1"; + variable = "$LoginName"; + altCommand = "newLoginProcess();"; + helpTag = "0"; + maxLength = "16"; + historySize = "0"; + password = "0"; + glowOffset = "9 9"; + }; + new GuiTextCtrl() { + profile = "ShellTextLeftProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "37 29"; + extent = "420 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Use the form below to log in with an existing key, retrieve a login key, or create"; + maxLength = "255"; + }; + new GuiTextCtrl() { + profile = "ShellTextLeftProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "37 45"; + extent = "420 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "a new account on the server."; + maxLength = "255"; + }; + new GuiTextCtrl(passTxt) { + profile = "ShellTextRightProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "37 107"; + extent = "85 22"; + minExtent = "8 8"; + visible = "1"; + helpTag = "0"; + text = "Password:"; + maxLength = "255"; + }; + new GuiLoginPasswordCtrl(LoginPasswordBox) { + profile = "NewTextEditProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "118 99"; + extent = "180 38"; + minExtent = "32 38"; + visible = "1"; + variable = "$LoginPassword"; + altCommand = "newLoginProcess();"; + helpTag = "0"; + maxLength = "255"; + historySize = "0"; + password = "1"; + glowOffset = "9 9"; + }; + new ShellToggleButton(rmbrPass) { + profile = "ShellRadioProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "122 134"; + extent = "167 27"; + minExtent = "26 27"; + visible = "1"; + variable = "$pref::RememberPassword"; + helpTag = "0"; + text = "REMEMBER PASSWORD"; + maxLength = "255"; + }; + new ShellBitmapButton() { + profile = "ShellButtonProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "300 69"; + extent = "147 38"; + minExtent = "32 38"; + visible = "1"; + command = "newLoginProcess();"; + helpTag = "0"; + text = "LOG IN"; + simpleStyle = "0"; + }; + new ShellBitmapButton() { + profile = "ShellButtonProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "300 99"; + extent = "147 38"; + minExtent = "32 38"; + visible = "1"; + command = "newCreateAccount();"; + helpTag = "0"; + text = "CREATE NEW ACCOUNT"; + simpleStyle = "0"; + }; + new ShellBitmapButton() { + profile = "ShellButtonProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "300 129"; + extent = "147 38"; + minExtent = "32 38"; + visible = "1"; + command = "quit();"; + accelerator = "escape"; + helpTag = "0"; + text = "QUIT"; + simpleStyle = "0"; + }; + }; + }; + + // Add these to the StartupGui to make sure everything gets cleaned up. + StartupGui.add(TN_logo); + StartupGui.add(ShellTextCenterProfile); + StartupGui.add(ShellTextLeftProfile); + StartupGui.add(noMoreModal); + // End UI replacements + + + +// Fill the drop-down list +function CA_strength::populate(%this) { + %this.add( "RSA-512", 0 ); + %this.add( "RSA-768", 1 ); + %this.add( "RSA-1024", 2 ); + %this.setSelected( 0 ); + %this.onSelect( 0, "RSA-512" ); +} +function CA_strength::onSelect( %this, %id, %text ) { + $keyStrength = %text; + %this.setText( %text ); + if (CreateAccountDlg.page != 2) return; + switch (%id) { + case 0: + %time = 2; + case 1: + %time = 3; + case 2: + %time = 7; + } + HintText2.setText("Your key could take up to "@%time@" minutes to create."); +} +function LoginEditMenu::populate(%this) { + %this.add( "Retrieve Account", 0 ); + + // LoginEditMenu.add( %name, %id ); + // Make sure to index keys to the number in the menu. 0 is used for key download. + // Use LoginEditMenu.size() for current length. + // + // When a new key is downloaded through t2csri_downloadAccount, try to have it use + // the setSelected/onSelect functions after adding to make the new field current and default + + // pull the list of accounts from the Ruby certificate store + rubyEval("tsEval '$accountList = \"' + certstore_listAccounts + '\";'"); + + %count = 0; + %accounts = getFieldCount($accountList); + for (%i = 0; %i < %accounts; %i++) + { + %this.add(getField($accountList, %i), %count++); + } + + if (%count < 1) %this.setActive(0); + %id = %this.findText( $LastLoginKey ); + if ( %id == -1 ) + %id = 0; + %text = %this.getTextById(%id); + %this.setSelected( %id ); + %this.onSelect( %id, %text ); + + // populate the game's alias selections for post-login + for (%i = 0; %i < %accounts; %i++) + { + %present = 0; + for (%j = 0; %j < $pref::Player::Count; %j++) + { + if (getField($pref::Player[%j], 0) $= getField($accountList, %i)) + %present = 1; + } + if (!%present) + { + $pref::Player[$pref::Player::Count] = getField($accountList, %i) @ "\tHuman Male\tbeagle\tMale1"; + $pref::Player::Count++; + } + } +} + +// Make sure everything is in the right place when an option is selected +function LoginEditMenu::onSelect( %this, %id, %text ) { + if (%id == 0) { + LoginPasswordBox.setPosition(118, 129); + passTxt.setPosition(37, 137); + accnTxt.setPosition(37, 107); + rmbrPass.setVisible(0); + LoginEditBox.setVisible(1); + } else { + LoginPasswordBox.setPosition(118, 99); + passTxt.setPosition(37, 107); + accnTxt.setPosition(37, 77); + rmbrPass.setVisible(1); + LoginEditBox.setVisible(0); + } + $LastLoginKey = %text; + %this.setText( %text ); +} +LoginEditMenu.populate(); +// Track the open state, and disable the next button unless ready to go. +function CreateAccountDlg::onWake( %this ) +{ + %this.open = true; + tn_EntropyBox.setBitmap("TN_entropy"); + CreateAccountDlg.bringToFront(tn_EntropyEvent); + // Check online status. + Authentication_checkAvail(); + // If it's online, set %this.online to true. + t2csri_checkOnlineStatusLoop(%this); +} +// this interfaces to the authentication interface script +function t2csri_checkOnlineStatusLoop(%this) +{ + // if no transaction to the authentication server is active... + if ($Authentication::Status::ActiveMode == 0) + { + %this.online = $Authentication::Status::Available; + CreateAccountNextBtn.setActive( false ); + updateNextButton(%this); + } + else + { + // otherwise, check again, as the transaction may still be in progress + schedule(128, 0, t2csri_checkOnlineStatusLoop, %this); + } +} + +function CreateAccountDlg::onSleep( %this ) +{ + %this.open = false; +} + +// All the account creation page junk is sent through here. +function CreateAccountDlg::nextBtn(%this,%reverse) { + CreateAccountNextBtn.setActive( false ); + %this.showFields[1] = false; + %this.showFields[2] = false; + %this.showFields[3] = false; + if(%reverse) %this.page--; + else %this.page++; + + %this.showFields[%this.page] = true; + switch (%this.page) { + case 1: + TitleBar.setText("Create Account - Step 1 of 3"); + CreateAccountPrevBtn.setValue(" CANCEL"); + CreateAccountNextBtn.setValue("NEXT STEP"); + %hintText = "Please wait as the server status is checked."; + HintText2.setText(%hintText); + %headtext = "Step One: Account Server Status"; + %body = "In order to create your account, the account server must be connectable. If it's offline, you won't be able to pass this step. \n\nIn step two, you will generate your unique key to ensure your account cannot be stolen.\n\nIn step three, you will choose your login information."; + AccountInstructions.setText(%headtext); + AccountText.setText(%body); + HintText.setVisible(1); + HintText.setPosition(125, 255); + HintText.setText(""); + + case 2: + TitleBar.setText("Create Account - Step 2 of 3"); + %headtext = "Step Two: "; + %body = ""; + AccountInstructions.setText(%headtext); + AccountText.setText(%body); + HintText.setVisible(1); + HintText.setPosition(120, 30); + HintText.setEntropyText(); + %keyText = $keyCreated ? "KEY GENERATED" : "GENERATE YOUR KEY"; + %active = (tn_EntropyEvent.finished && !$keyCreated) ? true : false; + if (!%active) HintText2.setText("Click the NEXT STEP button to proceed."); + CreateAccountGenBtn.setValue(%keyText); + CA_strength.setActive(%active); + CreateAccountGenBtn.setActive(%active); + CreateAccountPrevBtn.setValue("BACK"); + CreateAccountNextBtn.setValue("NEXT STEP"); + if (!CA_strength.size()) CA_strength.populate(); + logEntropy(); + + case 3: + TitleBar.setText("Create Account - Step 3 of 3"); + %headtext = "Step Three: Choose Your Account Details"; + %body = "Pick out your account details and confirm they are correct before registering your account. Don't forget your password."; + CA_keyName.setText("Strength: "@$keyStrength); + AccountInstructions.setText(%headtext); + AccountText.setText(%body); + HintText.setVisible(0); + CreateAccountGenBtn.setVisible(0); + CreateAccountPrevBtn.setValue("BACK"); + CreateAccountNextBtn.setValue("FINISH"); + HintText.setVisible(1); + HintText.setPosition(100, 290); + HintText.setText(""); + HintText2.setText("Fill out the above form to proceed."); + + + case 4: + // TODO: + // Send information to registration process: + // $CreateAccountLoginName + // $CreateAccountPassword + + LoginMessagePopup("PLEASE WAIT", "Registering Account with the Authentication Server..."); + t2csri_requestAccountSignature(%this); + + + default: + Canvas.popDialog( CreateAccountDlg ); + Canvas.pushDialog( LoginDlg ); + + } + CreateAccountGenBtn.setVisible(%this.showFields[2]); + CN_strength.setVisible(%this.showFields[2]); + CA_strength.setVisible(%this.showFields[2]); + tn_EntropyBox.setVisible(%this.showFields[2]); + CN_keyName.setVisible(%this.showFields[3]); + CA_keyName.setVisible(%this.showFields[3]); + CN_userName.setVisible(%this.showFields[3]); + CA_userName.setVisible(%this.showFields[3]); + CN_chooPass.setVisible(%this.showFields[3]); + CA_chooPass.setVisible(%this.showFields[3]); + CN_confPass.setVisible(%this.showFields[3]); + CA_confPass.setVisible(%this.showFields[3]); +} + +// ready to send the account to the server for processing, prepare it... +function t2csri_requestAccountSignature(%this) +{ + // pull the keys from the Ruby interpreter + rubyEval("tsEval '$e=\"' + $accountKey.e.to_s(16) + '\";'"); + rubyEval("tsEval '$n=\"' + $accountKey.n.to_s(16) + '\";'"); + rubyEval("tsEval '$d=\"' + $accountKey.d.to_s(16) + '\";'"); + $encryptedExponent = t2csri_encryptAccountKey($d, $CreateAccountPassword); + %authSHA = sha1sum("3.14159265" @ trim(strlwr($CreateAccountLoginName)) @ $CreateAccountPassword); + %reqsig = $CreateAccountLoginName @ "\t" @ $e @ "\t" @ $n @ "\t" @ $encryptedExponent @ "\t" @ %authSHA; + + // delete the variables + $e = ""; + $d = ""; + $n = ""; + + // (RC2) perform a signature operation on the fields from the name to the end + %requestSHA1 = sha1sum(%reqsig); + rubyEval("tsEval '$requestRSA=\"' + $accountKey.decrypt('" @ %requestSHA1 @ "'.to_i(16)).to_s(16) + '\";'"); + %reqsig = %reqsig @ "\t" @ $requestRSA; + + //echo("Request: " @ %reqsig); + $Authentication::Status::LastCert = ""; + Authentication_registerAccount(%reqsig); + schedule(512, 0, t2csri_completeAccountRequest, %this); +} + +function t2csri_completeAccountRequest(%this) +{ + // if no transaction to the authentication server is active... + if ($Authentication::Status::ActiveMode == 0) + { + popLoginMessage(); + if (strLen($Authentication::Status::LastCert) > 0) + { + // success + LoginMessagePopup("SUCCESS", "Account generated successfully. Storing account data to disk and logging in..."); + schedule(3000, 0, popLoginMessage); + schedule(3000, 0, LoginDone); + + // store the account data to file + %username = getField($Authentication::Status::LastCert, 0); + rubyEval("certstore_addAccount('" @ $Authentication::Status::LastCert @ "','" @ %username @ "\t" @ $encryptedExponent @ "')"); + // protect the key... now that we have succeeded + $LoginCertificate = $Authentication::Status::LastCert; + rubyEval("$accountKey.protect"); + } + else + { + // handle the error + if ($Authentication::Status::Signature $= "Server chose to reject account generation request.") + { + LoginMessagePopup("ERROR", "The Authentication Server understood your request, but chose not to fulfill it."); + } + else if ($Authentication::Status::Signature $= "Server rejected account name.") + { + LoginMessagePopup("ERROR", "The Authentication Server rejected your requested account name."); + } + else if ($Authentication::Status::Signature $= "Corrupt signature request rejected.") + { + LoginMessagePopup("ERROR", "The server detected a problem in your request and could not create an account."); + } + else if ($Authentication::Status::Signature $= "Unknown signature status code returned from server.") + { + LoginMessagePopup("ERROR", "The Authentication Server timed out while fulfilling your request."); + } + // go back to the account page + %this.nextBtn(1); + // schedule a "pop" of the error box we just put up + schedule(7000, 0, popLoginMessage); + } + } + else + { + // otherwise, check again, as the transaction may still be in progress + schedule(128, 0, t2csri_completeAccountRequest, %this); + } +} + +function HintText::setEntropyText( %this ) +{ + if (tn_EntropyEvent.finished) + { + %lines = "1. Select your key strength.\n2. Click the generate button."; + CreateAccountGenBtn.setActive(1); + CA_strength.setActive(1); + + } + else + { + if(tn_EntropyEvent.time $= "") + tn_EntropyEvent.time = 80; + %lines = (tn_EntropyEvent.hasMouse ? "":"") @ "1. Move your mouse inside the big box."; + %lines = %lines NL "2. Wiggle it around for "@mCeil(tn_EntropyEvent.time / 8)@" more seconds."; + } + HintText.setText(%lines); +} +function tn_EntropyEvent::onMouseEnter(%this, %mod, %pos, %count) +{ + if (tn_EntropyEvent.finished) + return; + tn_EntropyEvent.hasMouse = true; + HintText.setEntropyText(); +} +function tn_EntropyEvent::onMouseLeave(%this, %mod, %pos, %count) +{ + if (tn_EntropyEvent.finished) + return; + tn_EntropyEvent.hasMouse = false; + HintText.setEntropyText(); +} +function logEntropy() +{ + if (tn_EntropyEvent.finished) + return; + + // Ruby Invocation Happens Here... + // first call of this function... build the Mersenne Twister RNG in Ruby + if (!$rubyRNGCreated) + { + $rubyRNGCreated = 1; + rubyEval("$twister = MersenneTwister.new"); + rubyEval("$entropy = 0"); + } + + if ( CreateAccountDlg.page != 2 || !CreateAccountDlg.open ) + return; + if ( tn_EntropyEvent.lastPos $= canvas.getCursorPos() ) + { + schedule(128, 0, logEntropy); + return; + } + if ( tn_EntropyEvent.hasMouse ) + { + tn_EntropyEvent.lastPos = canvas.getCursorPos(); + tn_EntropyEvent.time--; + if (strstr( tn_EntropyEvent.time, 0) != -1) + { + %pos = canvas.getCursorPos(); + %bit = new GuiBitmapCtrl() { + profile = "noMoreModal"; + bitmap = "texticons/bullet_2"; + extent = "19 18"; + visible = true; + opacity = "0.25"; + minExtent = "19 18"; + helpTag = "0"; + wrap = true; + }; + tn_EntropyBox.add(%bit); + %bit.setPosition(getWord(%pos,0)-365,getWord(%pos,1)-320); + } + %entropy = strreplace(canvas.getCursorPos()," ",""); + // Ruby Invocation Happens Here... + // add the current screen coordinate to the entropy pool + rubyEval("$entropy = $entropy + " @ %entropy); + if ( tn_EntropyEvent.time == 0 ) + { + rubyEval("$entropy = $entropy + " @ getRealTime()); + //rubyEval("puts $entropy % 4294967296"); + rubyEval("$twister.seedgen($entropy % 4294967296)"); + tn_EntropyEvent.finished = true; + beginEntropyWait(); + } + else + schedule(128,0, logEntropy); + HintText.setEntropyText(); + } + else + schedule(128,0, logEntropy); +} + +// churn the RNG state for additional entropy +function beginEntropyWait() +{ + if (CreateAccountDlg.page != 2 || $keyCreated) + return; + if (isEventPending($entropyWait)) + { + cancel($entropyWait); + } + $entropyWait = schedule(256, 0, beginEntropyWait); + + rubyEval("$twister.randomnumber(160)"); +} + +// Warrior name check. Useful to keep entry valid. +function CA_userName::validateWarriorName( %this ) +{ + %name = %this.getValue(); + %test = strToPlayerName( %name ); + if ( %name !$= %test ) + %this.setText( %test ); +} + +// If the options aren't in, disable the button. +function updateNextButton() +{ + if ( !CreateAccountDlg.open ) + return; + + %done = true; + switch (CreateAccountDlg.page) + { + case 1: + if (!$RubyEnabled) + { + HintText.setText("Your game is not running the patched executable."); + HintText2.setText("Close the game and verify it is patched."); + %done = false; + } + else if ($AuthServer::Address $= "") + { + HintText.setText("The server address has not yet been retrieved."); + HintText2.setText("Close this page and try again in a moment."); + authConnect_findAuthServer(); + %done = false; + } + else if (!CreateAccountDlg.online) + { + if (CreateAccountDlg.online !$= "") + { + HintText.setText("The account server is OFFLINE or unreachable."); + HintText2.setText("Check your network connection and try again."); + } + %done = false; + } + else + { + HintText.setText("The account server is ONLINE and connectable."); + HintText2.setText("Click the NEXT STEP button to proceed."); + } + + case 2: + if (!$keyCreated) %done = false; + + case 3: + if (strlen($CreateAccountLoginName) < 4) + { + %done = false; + if (strlen($CreateAccountLoginName) > 0) + HintText.setText("Error: Your username must be at least 4 characters long."); + else + HintText.setText(""); + } + else if (strlen($CreateAccountPassword) < 6) + { + %done = false; + if (strlen($CreateAccountPassword) > 0) + HintText.setText("Error: Your password must be at least 6 characters long."); + else + HintText.setText(""); + } + else if (strcmp($CreateAccountPassword, $CreateAccountConfirmPassword)) + { + %done = false; + if (strlen($CreateAccountConfirmPassword) > 0) + HintText.setText("Error: Your password confirmation doesn't match."); + else + HintText.setText(""); + } + else + { + if ($CreateAccountLastEnteredUsername !$= $CreateAccountLoginName) + { + // client has typed in a new name... test suitability with the auth server + $CreateAccountLastEnteredUsername = $CreateAccountLoginName; + $Authentication::Status::Name = ""; + $NameSuitabilityMode = 1; + Authentication_checkName($CreateAccountLoginName); + t2csri_testNameSuitability(); + } + if ($NameSuitabilityMode) + { + HintText.setText(""); + %done = false; + } + if ($Authentication::Status::Name !$= "Name is available and acceptable.") + { + %status = ($Authentication::Status::Name $= "") ? "Checking name for availability..." : "Error:" SPC $Authentication::Status::Name; + HintText.setText(%status); + %done = false; + } + } + + } + CreateAccountNextBtn.setActive( %done ); + + schedule( 1000, 0, updateNextButton ); +} + +function t2csri_testNameSuitability() +{ + // if no transaction to the authentication server is active... + if ($Authentication::Status::ActiveMode == 0) + { + if ($Authentication::Status::Name !$= "Name is available and acceptable.") + %status = "Error: "; + else + %status = "Success: "; + HintText.setText(%status @ $Authentication::Status::Name); + $NameSuitabilityMode = 0; + } + else + { + // otherwise, check again, as the transaction may still be in progress + schedule(128, 0, t2csri_testNameSuitability); + } +} + +function CreateAccountDlg::genBtn(%this) +{ + LoginMessagePopup( "Creating your key...", "This can take a few minutes.\nDO NOT EXIT THE GAME\n" ); + schedule( 2000, 0, popLoginMessage ); + // Ruby Invocation Happens Here... + // Pass this through to the key generation function. + // $keyStrength + $keyStrength = getSubStr($keyStrength, 4, strlen($keyStrength)); + rubyEval("$accountKey = RSAKey.new"); + rubyEval("$accountKey.twister = $twister"); + cancel($entropyWait); + schedule(1024, 0, rubyEval, "$accountKey.generate(" @ $keyStrength @ ")"); + + // When done, have the following set: + $keyCreated = true; + CA_strength.setActive(!$keyCreated); + CreateAccountGenBtn.setActive(!$keyCreated); + CreateAccountGenBtn.setValue("KEY GENERATED"); + HintText2.setText("Click the NEXT STEP button to proceed."); + CreateAccountNextBtn.setActive(1); +} +function popLoginMessage() +{ + Canvas.popDialog( LoginMessagePopupDlg ); +} +function newCreateAccount() +{ + $CreateAccountLoginName = ""; + $CreateAccountPassword = ""; + $CreateAccountConfirmPassword = ""; + Canvas.pushDialog( CreateAccountDlg ); + Canvas.popDialog( LoginDlg ); + CreateAccountDlg.page = 0; + CreateAccountDlg.nextBtn(); +} +function newLoginProcess() +{ + if (!$RubyEnabled) + { + MessageBoxOK("LOGIN ERROR","Your game is not running the patched game executable.\n\nClose the game and verify the patch was run successfully."); + return; + } + if (LoginEditMenu.getSelected() == 0) + { + if ( strlen( $LoginName ) < 3 ) + return; + else + { + if ( LoginEditMenu.findText( $LoginName ) == -1 ) + MessageBoxYesNo("Connect Account","That account isn't stored locally, would you like to retrieve it from the account server?","t2csri_downloadAccount($LoginName, $LoginPassword);",""); + else + { + LoginMessagePopup( "PLEASE WAIT", "Logging in..." ); + schedule(128, 0, t2csri_doLogin, $LoginName, $LoginPassword); + } + } + } + else + { + if ( $pref::RememberPassword ) + LoginPasswordBox.savePassword(); + LoginMessagePopup( "PLEASE WAIT", "Logging in..." ); + schedule(128, 0, t2csri_doLogin, $LastLoginKey, $LoginPassword); + } +} + + +function t2csri_doLogin(%username, %password) +{ + //warn(%username SPC %password); + %status = t2csri_getAccount(%username, %password); + warn(%status); + if (%status $= "SUCCESS") + { + // continue login + $pref::LastLoginKey = $LastLoginKey; + export( "$pref::*", "prefs/ClientPrefs.cs", false); + Canvas.popDialog(LoginDlg); + schedule(128, 0, popLoginMessage); + schedule(128, 0, LoginDone); + + // set the active "alias" to the current username + for (%i = 0; %i < $pref::Player::Count; %i++) + { + if (getField($pref::Player[%i], 0) $= trim(%username)) + $pref::Player::Current = %i; + } + } + else if (%status $= "INVALID_PASSWORD") + { + // pop-up a dialog asking the player to try again + popLoginMessage(); + LoginMessagePopup( "INVALID PASSWORD", "The password you entered is not correct. Try again." ); + schedule(3000, 0, popLoginMessage); + } + else + { + popLoginMessage(); + LoginMessagePopup( "ERROR", "An unknown error occured. Status code: " @ %status); + schedule(3000, 0, popLoginMessage); + } +} diff --git a/scripts/autoexec/t2csri_IRCfix.cs b/scripts/autoexec/t2csri_IRCfix.cs new file mode 100644 index 0000000..15524b9 --- /dev/null +++ b/scripts/autoexec/t2csri_IRCfix.cs @@ -0,0 +1,185 @@ +$IRCClient::NickName = getField(wonGetAuthInfo(),0); +$IRCClient::NickName = strReplace($IRCClient::NickName," ","_"); +$IRCClient::NickName = stripChars($IRCClient::NickName,"~@#$!+%/|^{&*()<>"); + +package t2csri_ircfix { +function GetIRCServerList(%arg1) { + return "IP:irc.arloria.net:6667"; +} +function IRCClient::notify(%event) +{ + if (isObject(ServerConnection) && getSubStr(%event,0,9) $= "IDIRC_ERR") return; + Parent::notify(%event); +} +function IRCClient::away(%params) +{ + %me = $IRCClient::people.getObject(0); + %me.flags = %me.flags & ~$PERSON_AWAY; + if (strlen(%params)) + { + if ($IRCClient::awaytimeout) + { + cancel($IRCClient::awaytimeout); + $IRCClient::awaytimeout = 0; + } + IRCClient::send("AWAY :" @ %params); + } else IRCClient::send("AWAY"); +} +function IRCTCP::onDisconnect(%this) +{ + $IRCClient::state = IDIRC_DISCONNECTED; + IRCClient::reset(); + //IRCClient::notify(IDIRC_ERR_DROPPED); + parent::onDisconnect(%this); +} +function IRCTCP::onConnected(%this) +{ + IRCClient::newMessage("","IRCClient: Established TCP/IP connection"); + %me = $IRCClient::people.getObject(0); + %me.displayName = $IRCClient::NickName; + %me.setName(%me.displayName); + $IRCClient::tcp.schedule(500, "send", "NICK " @ $IRCClient::NickName @ "\r\n"); + $IRCClient::tcp.schedule(500, "send", "USER " @ $IRCClient::NickName @ " x x :" @ $IRCClient::NickName @ "\r\n"); + $IRCClient::tcp.schedule(2000, "send", "WHOIS " @ $IRCClient::NickName @ "\r\n"); + $IRCClient::state = IDIRC_CONNECTING_WAITING; +} +function IRCClient::relogin() +{ + if($IRCClient::state !$= IDIRC_CONNECTED) + IRCClient::connect(); + %me = $IRCClient::people.getObject(0); + %me.displayName = $IRCClient::NickName; + %me.setName(%me.displayName); + %me.tagged = %me.displayName; + IRCClient::correctNick(%me); + IRCClient::newMessage("","IRCClient: Reauthentication starting"); + $IRCClient::tcp.schedule(500, "send", "NICK " @ $IRCClient::NickName @ "\r\n"); + $IRCClient::tcp.schedule(500, "send", "USER " @ $IRCClient::NickName @ " x x :" @ $IRCClient::NickName @ "\r\n"); + $IRCClient::tcp.schedule(2000, "send", "WHOIS " @ $IRCClient::NickName @ "\r\n"); + $IRCClient::state = IDIRC_CONNECTING_WAITING; +} +function IRCClient::dispatch(%prefix,%command,%params) +{ + if (%command == 378) {IRCClient::onConFrom(%prefix,%params); return true;} + else parent::dispatch(%prefix,%command,%params); +} +function chatMemberPopup::add(%this,%name,%index) { + if (%index == 10 || %index == 11) return; + parent::add(%this,%name,%index); +} +function JoinChatDlg::onWake(%this) +{ + if ($IRCClient::state $= IDIRC_CONNECTING_WAITING) + MessageBoxOK("CONNECTING...","Waiting for IRC server to respond, please wait."); + else + parent::onWake(%this); +} +function ChatTabView::onSelect(%this,%obj,%name) +{ + parent::onSelect(%this,%obj,%name); + if (%name $= "welcome" && $IRCClient::channels.getObject(0) != %obj) + { + ChatPanel.setVisible(true); + WelcomePanel.setVisible(false); + ChatEditOptionsBtn.setVisible(false); + } +} +function IRCClient::onConFrom(%prefix,%params) +{ + //IP acquisition test... may remove + //Krash-T2 Krash-T2 :is connecting from *@24.108.153.184 24.108.153.184 + if ($IPv4::InetAddress $= "" && getWord(%params,0) $= $IRCClient::people.getObject(0).displayName) $IPv4::InetAddress = getWord(%params,getWordCount(%params)-1); +} +function IRCClient::onBadNick(%prefix,%params) +{ + $IRCClient::NickName = getField(wonGetAuthInfo(),0) @ "-"@getRandom(0,99); + $IRCClient::NickName = strReplace($IRCClient::NickName," ","_"); + IRCClient::relogin(); +} +function IRCClient::onNick(%prefix,%params) +{ + %person = IRCClient::findPerson2(%prefix,false); + if (%person) { + %person.displayName = %params; + %person.tagged = %params; + IRCClient::correctNick(%person); + ChatRoomMemberList_rebuild(); + } + parent::onNick(%prefix,%params); + +} +function IRCClient::newMessage(%channel,%message) +{ + //quick UE fix, rewrite later + for (%i = 0;%i < getWordCount(%message);%i++) { + %word = getWord(%message,%i); + %first = strstr(%word,"<"); + if (%first != -1) { + %word1 = getSubstr(%word,%first,strlen(%word)); + %second = strstr(%word1,">"); + if (%second == -1) + %message = stripChars(%message,"<>"); + } + } + parent::newMessage(%channel,%message); +} +function IRCClient::setIdentity(%p,%ident) +{ + parent::setIdentity(%p,%ident); + if(%p.getName() !$= %p.displayName) %p.setName(%p.displayName); + if(%p.untagged $= "")%p.untagged = %p.displayName; +} +function IRCClient::onMode(%prefix,%params) +{ + parent::onMode(%prefix,%params); + ChatRoomMemberList_rebuild(); +} +function IRCClient::onJoinServer(%mission,%server,%address,%mayprequire,%prequire) +{ + if(strstr(strlwr($IRCClient::currentChannel.getName(),"tribes")) != -1) return; + parent::onJoinServer(%mission,%server,%address,%mayprequire,%prequire); +} +function IRCClient::onNameReply(%prefix,%params) +{ + + %params = strreplace(%params,"~","@"); + %params = strreplace(%params,"&","@"); + %params = strreplace(%params,"*","@"); + %params = strreplace(%params,"%","@"); + %params = strreplace(%params,"^","@"); + parent::onNameReply(%prefix,%params); +} +function IRCClient::onPing(%prefix,%params) +{ + //echo(%prefix SPC %params); + if (!$PingStarted) { + $IRCClient::tcp.schedule(1000, "send", "PONG " @ %params @ "\r\n"); + $PingStarted = true; + } else $IRCClient::tcp.send("PONG " @ %params @ "\r\n"); + +} +function IRCClient::onPart(%prefix,%params) +{ + %params = firstWord(%params); + parent::onPart(%prefix,%params); + ChatRoomMemberList_rebuild(); +} +function IRCClient::notify(%event) +{ + if (%event $= IDIRC_CHANNEL_LIST) { + JoinChatList.clear(); + for (%i = 0; %i < $IRCClient::numChannels; %i++) + { + switch$ ( $IRCClient::channelNames[%i] ) { + case "#the_construct" or "#help" or "#welcome": %temp = 1; + default: %temp = 0; + } + if (strStr(strlwr($IRCClient::channelNames[%i]),"tribes") != -1) %temp = 1; + JoinChatList.addRow(%i, IRCClient::displayChannel( $IRCClient::channelNames[%i]) TAB $IRCClient::channelUsers[%i] TAB %temp ); + JoinChatList.setRowStyle( %i, %temp > 0 ); + } + JoinChatList.sort(); + JoinChatName.onCharInput(); + } else parent::notify(%event); +} +}; activatePackage(t2csri_ircfix); diff --git a/scripts/autoexec/t2csri_list.cs b/scripts/autoexec/t2csri_list.cs new file mode 100644 index 0000000..b7ea344 --- /dev/null +++ b/scripts/autoexec/t2csri_list.cs @@ -0,0 +1,459 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Krash +// Copyright 2008 by Krash and the Tribes 2 Community System Reengineering Intitiative + +// Master listing / Queries. + +if ($Host::TN::beat $= "") $Host::TN::beat = 3; //Time between beats in minutes. +if ($Host::TN::echo $= "") $Host::TN::echo = 1; //Enable the MS echoes. + + +function NewsGui::onWake( %this ) +{ + Canvas.pushDialog( LaunchToolbarDlg ); + %this.pane = "News"; + NM_TabView.setSelected( 1 ); +} +function NM_TabView::onAdd( %this ) +{ + %this.addSet( 1, "gui/shll_horztabbuttonB", "5 5 5", "50 50 0", "5 5 5" ); + %this.addTab(1,"NEWS",1); + %this.addTab(2,"FORUMS"); + %this.setTabActive(2,0); + %this.addTab(3,"DOWNLOADS"); + %this.setTabActive(3,0); +} +function NM_TabView::onSelect( %this, %id, %text ) +{ + NM_NewsPane.setVisible( %id == 1 ); + //NM_ForumPane.setVisible( %id == 2 ); + //NM_FilePane.setVisible( %id == 3 ); + NM_TabFrame.setAltColor( %id == 1 ); + + %ctrl = "NM_" @ NewsGui.pane @ "Pane"; + if ( isObject( %ctrl ) ) + %ctrl.onDeactivate(); + + switch ( %id ) + { + case 1: // News + NM_NewsPane.onActivate(); + } +} +function NM_NewsPane::onActivate(%this) { + NewsGui.pane = "News"; + +} +function NM_NewsPane::onDeactivate(%this) {} +function NewsGui::setKey(%this) {} +function LaunchNews() { +if (!isObject(NewsGui)){ +new GuiChunkedBitmapCtrl(NewsGui) { + profile = "GuiContentProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "0 0"; + extent = "640 480"; + minExtent = "8 8"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + variable = "$ShellBackground"; + helpTag = "0"; + useVariable = "1"; + + new ShellPaneCtrl() { + profile = "ShellPaneProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "12 13"; + extent = "620 423"; + minExtent = "48 92"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + text = "TRIBESNEXT"; + maxLength = "255"; + noTitleBar = "0"; + + + new ShellTabFrame(NM_TabFrame) { + profile = "ShellHorzTabFrameProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "22 54"; + extent = "576 351"; + minExtent = "26 254"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + isVertical = "0"; + useCloseButton = "0"; + edgeInset = "0"; + }; + new ShellTabGroupCtrl(NM_TabView) { + profile = "TabGroupProfile"; + horizSizing = "width"; + vertSizing = "bottom"; + position = "30 25"; + extent = "560 29"; + minExtent = "38 29"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + glowOffset = "7"; + tabSpacing = "2"; + maxTabWidth = "150"; + stretchToFit = "0"; + }; + new GuiControl(NM_NewsPane) { + profile = "GuiDefaultProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "0 0"; + extent = "586 423"; + minExtent = "8 8"; + visible = "0"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + + new ShellFieldCtrl(NewsPanel) { + profile = "ShellFieldProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "31 92"; + extent = "559 315"; + minExtent = "16 18"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + + new ShellScrollCtrl() { + profile = "NewScrollCtrlProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "195 5"; + extent = "360 303"; + minExtent = "24 52"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + willFirstRespond = "1"; + hScrollBar = "alwaysOff"; + vScrollBar = "alwaysOn"; + constantThumbHeight = "0"; + defaultLineHeight = "15"; + childMargin = "0 2"; + fieldBase = "gui/shll_field"; + + new GuiScrollContentCtrl() { + profile = "GuiDefaultProfile"; + horizSizing = "width"; + vertSizing = "height"; + position = "4 6"; + extent = "336 291"; + minExtent = "8 8"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + + new GuiMLTextCtrl(NewsText) { + profile = "NewTextEditProfile"; + horizSizing = "width"; + vertSizing = "bottom"; + position = "0 0"; + extent = "362 2376"; + minExtent = "8 8"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + lineSpacing = "2"; + allowColorChars = "0"; + maxChars = "-1"; + deniedSound = "InputDeniedSound"; + }; + }; + }; + new ShellScrollCtrl() { + profile = "NewScrollCtrlProfile"; + horizSizing = "right"; + vertSizing = "height"; + position = "2 21"; + extent = "195 287"; + minExtent = "24 52"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + willFirstRespond = "1"; + hScrollBar = "alwaysOff"; + vScrollBar = "dynamic"; + constantThumbHeight = "0"; + defaultLineHeight = "15"; + childMargin = "0 3"; + fieldBase = "gui/shll_field"; + + new GuiScrollContentCtrl() { + profile = "GuiDefaultProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "4 7"; + extent = "187 273"; + minExtent = "8 8"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + + new ShellTextList(NewsHeadlines) { + profile = "ShellTextArrayProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "0 0"; + extent = "187 180"; + minExtent = "8 8"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + enumerate = "0"; + resizeCell = "1"; + columns = "0"; + fitParentWidth = "1"; + clipColumnText = "0"; + }; + }; + }; + new GuiTextCtrl() { + profile = "ShellAltTextProfile"; + horizSizing = "right"; + vertSizing = "bottom"; + position = "12 6"; + extent = "72 20"; + minExtent = "8 8"; + visible = "1"; + hideCursor = "0"; + bypassHideCursor = "0"; + helpTag = "0"; + text = "HEADLINES:"; + longTextBuffer = "0"; + maxLength = "255"; + }; + }; + + }; + + + }; +}; +} else LaunchTabView.viewTab( "TRIBESNEXT", NewsGui, 0 ); +} +//================================================================ + +function queryTNServers(%filter,%mod,%maptype,%minplayers,%maxplayers,%maxBots,%flags) { + + %server = "master.tribesnext.com:80"; + if (!isObject(TNbite)) + %bite = new TCPObject(TNbite){}; + else %bite = TNbite; + %bite.mode = 0; + %filename = "/list"; + if (%filter) + %filename = "/list/"@%mod@"/"@%maptype@"/"@%minplayers@"/"@%maxplayers@"/"@%maxBots@"/"@%flags; + if (%filter $= "types") { + %filename = "/listtypes"; + %bite.mode = 2; + } else queryFavoriteServers(); // Filtering fix, since the old master query isn't used. + + %bite.get(%server, %filename); +} + +function queryMasterGameTypes(){ + clearGameTypes(); + clearMissionTypes(); + queryTNServers("types"); +} + +function queryMasterServer(%port, %flags, %rulesSet, %missionType, %minPlayers, %maxPlayers, %maxBots, %regionMask, %maxPing, %minCpu, %filtFlags, %buddy ) +{ + if (%flags !$= "") queryTNServers(1,%rulesSet,%missionType,%minplayers,%maxplayers,%maxBots,%filtFlags SPC %buddy); + else queryTNServers(); +} + +function TNbite::onLine(%this, %line) { + if (trim(%line) $= "") { + if (!%this.primed) %this.primed = true; + if (%this.mode != 5) return; + } + if (!%this.primed) return; + + if (%this.mode == 1) + switch (%line) { // heartbeats + case 0: if ($Host::TN::echo) echo(" - Server added to list."); + case 1: if ($Host::TN::echo) { echo(" - Your server could not be contacted."); + echo(" - Check your IP / port configuration."); } + case 2: if ($Host::TN::echo) echo(" - Heartbeat confirmed."); + } + else if (%this.mode == 2) //filter retrieval + switch (firstWord(%line)) { + case 0: addGameType( restWords(%line) ); + case 1: addMissionType( restWords(%line) ); + } + else if (%this.mode == 5) // news retrieval + NewsGui.addLine(%line); + else // and finally, the server list... + if ( strpos(%line,":") != -1 && strstr(%line,".") != -1) { + querySingleServer( %line ); + if (!%this.fnd) %this.fnd = true; + } +} + +function TNbite::onConnectFailed(%this) { + if ($Host::TN::echo) echo("-- Could not connect to master server."); +} + +function TNbite::onDNSFailed(%this) { + if ($Host::TN::echo) echo("-- Could not connect to DNS server."); +} + +function TNbite::onDisconnect(%this) { + if (!%this.fnd && %this.mode == 0) + if (!GMJ_Browser.rowCount()) + updateServerBrowserStatus( "No servers found.", 0 ); + %this.delete(); +} + +function NewsGui::addLine( %this, %line ) { + %this = NewsText; + if (firstWord(%line) $= "") { + %line = setWord(%line,0,""); + NewsHeadlines.addRow(%this.index,stripMLControlChars(%line)); + } + if (%line $= "#EOF") {NewsText.upToDate = true; NewsHeadlines.setSelectedRow(0); return;} + %text = %this.getText(); + %line = detag( %line ); + %text = (%text $= "") ? %line : %text NL %line; + %this.setText( %text ); +} + +function NewsText::update( %this, %online ) { + %this.setText(""); + NewsHeadlines.clear(); + %this.index = -1; + if (%online) { + %server = "www.tribesnext.com:80"; + if (!isObject(TNbite)) + %bite = new TCPObject(TNbite){}; + else %bite = TNbite; + %bite.mode = 5; + %filename = "/news"; + %bite.get(%server, %filename); + } +} +function NewsHeadlines::onSelect( %this, %id, %text ) +{ + NewsText.scrollToTag( %id ); +} +//================================================================ +package t2csri_webs { + +function TNbite::get(%this, %server, %query) +{ + if ($t2csri::isOfflineMode) + { + warn("TribesNext: Running in offline mode. Aborting query to the Master List Server."); + return; + } + %this.server = %server; + %this.query = %query; + %this.connect(%server); +} + +function TNbite::onConnected(%this) +{ + if (%this.query !$= "") { + %query = "GET " @ %this.query @ " HTTP/1.1\r\nHost: " @ %this.server @ "\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + %this.send(%query); + } +} + +function LaunchTabView::addLaunchTab( %this, %text, %gui, %makeInactive ) { + // disable currently unused tabs + //if (%text $= "EMAIL" || %text $= "BROWSER") parent::addLaunchTab( %this, %text, %gui, 1 ); + if (%text $= "BROWSER") parent::addLaunchTab( %this, %text, %gui, 1 ); + else parent::addLaunchTab( %this, %text, %gui, %makeInactive ); +} +function LaunchToolbarMenu::add(%this,%id,%text) { + parent::add(%this,%id,%text); + if ($PlayingOnline && %text $= "BROWSER") { + LaunchToolbarMenu.add( 1, "TRAINING" ); + LaunchToolbarMenu.add( 2, "TRIBESNEXT" ); + } +} + +function OpenLaunchTabs( %gotoWarriorSetup ) { + parent::OpenLaunchTabs( %gotoWarriorSetup ); + if ($PlayingOnline && !TrainingGui.added) { + LaunchTabView.addLaunchTab( "TRAINING", TrainingGui ); + LaunchTabView.addLaunchTab( "TRIBESNEXT", NewsGui ); + LaunchNews(); + NewsText.update(1); + TrainingGui.added = true; + } +} + +function JoinSelectedGame() { + if (($IPv4::InetAddress $= "" || strstr($IPv4::InetAddress,".") == -1) && $PlayingOnline) { + messageBoxOK("IP ERROR","Your external address has not been set or is set incorrectly. \n\nAttempting to reset..."); + ipv4_getInetAddress(); + return; + } else parent::JoinSelectedGame(); +} +function ClientReceivedDataBlock(%index, %total) +{ + DB_LoadingProgress.setValue( %index / %total ); + parent::ClientReceivedDataBlock(%index, %total); +} + +function CreateServer(%mission, %missionType) { + parent::CreateServer(%mission, %missionType); + if (!isActivePackage(t2csri_server)) schedule(2000,0,"exec","t2csri/serverGlue.cs"); +} + +function StartHeartbeat() { + if ($playingOnline) { + if(isEventPending($TNBeat)) cancel($TNBeat); + %server = "master.tribesnext.com:80"; + if ($Host::BindAddress !$= "") + %path = "/add/" @ $Host::Port @"/"@ $Host::BindAddress; + else %path = "/add/" @ $Host::Port; + if (!isObject(TNbite)) + %bite = new TCPObject(TNbite){}; + else %bite = TNbite; + %bite.mode = 1; + %bite.get(%server, %path); + if ($Host::TN::echo) + echo("-- Sent heartbeat to TN Master. ("@%server@")"); + $TNBeat = schedule($Host::TN::beat*60000,0,"StartHeartBeat"); + } else parent::StartHeartbeat(); +} + +function StopHeartbeat() { + if ($playingOnline) { + if(isEventPending($TNBeat)) cancel($TNBeat); + } else parent::StartHeartbeat(); +} +//================================================================ +}; +if (!isActivePackage(t2csri_webs)) activatepackage (t2csri_webs); + +exec("t2csri/postLogin.cs"); \ No newline at end of file diff --git a/scripts/autoexec/t2csri_serv.cs b/scripts/autoexec/t2csri_serv.cs new file mode 100644 index 0000000..4caa3f4 --- /dev/null +++ b/scripts/autoexec/t2csri_serv.cs @@ -0,0 +1,8 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 1.0 initialization and glue file (server side) + +schedule(0, 0, exec, "t2csri/serverglue.cs"); \ No newline at end of file diff --git a/t2csri/authconnect.cs b/t2csri/authconnect.cs new file mode 100644 index 0000000..ee25430 --- /dev/null +++ b/t2csri/authconnect.cs @@ -0,0 +1,87 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Authentication Server Connector Version 1.0: 11/06/2008 + +function authConnect_findAuthServer() +{ + if ($AuthServer::Address !$= "") + return; + echo("Looking up Authentication Server..."); + if (isObject(AuthConnection)) + { + AuthConnection.disconnect(); + AuthConnection.delete(); + } + new TCPObject(AuthConnection); + + %data = "GET /auth HTTP/1.1\r\nHost: www.tribesnext.com\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + AuthConnection.data = %data; + AuthConnection.connect("www.tribesnext.com:80"); + $AuthServer::Primed = 0; +} + +function AuthConnection::onLine(%this, %line) +{ + if (%line == 411) + return; + if (trim(%line) $= "") + { + $AuthServer::Primed = 1; + return; + } + + if ($AuthServer::Primed) + { + $AuthServer::Address = %line; + %this.disconnect(); + authConnect_verifyLookup(); + } +} + +function AuthConnection::onConnected(%this) +{ + %this.send(%this.data); +} + +function authConnect_verifyLookup() +{ + + if (getFieldCount($AuthServer::Address) != 2) + { + $AuthServer::Address = ""; + error("Authentication server lookup failed."); + return; + } + %address = getField($AuthServer::Address, 0); + %signature = getField($AuthServer::Address, 1); + + %sha1sum = sha1sum(%address); + %verifSum = t2csri_verify_auth_signature(%signature); + while (strlen(%verifSum) < 40) + %verifSum = "0" @ %verifSum; + if (%sha1sum !$= %verifSum) + { + // signature verification failed... someone has subverted the auth server lookup + error("Authentication server lookup returned an address with an invalid signature."); + error("Unable to contact the authentication server."); + $AuthServer::Address = ""; + return; + } + else + { + echo("Authentication server found at " @ %address @ ". Ready to authenticate."); + $AuthServer::Address = %address; + $AuthServer::Primed = ""; + } +} + +// perform signature verification to prove that the auth server has designated the +// provided address +function t2csri_verify_auth_signature(%sig) +{ + rubyEval("tsEval '$temp=\"' + t2csri_verify_auth_signature('" @ %sig @ "').to_s(16) + '\";'"); + return $temp; +} diff --git a/t2csri/authinterface.cs b/t2csri/authinterface.cs new file mode 100644 index 0000000..8d456a0 --- /dev/null +++ b/t2csri/authinterface.cs @@ -0,0 +1,238 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Authentication Server Interface Version 1.0: 12/29/2008 + +$Authentication::Mode::Available = 1; +$Authentication::Mode::Name = 2; +$Authentication::Mode::Recover = 3; +$Authentication::Mode::Sign = 4; + +$Authentication::Settings::Timeout = 30000; + +function AuthenticationInterface::onLine(%this, %line) +{ + //warn(%line); + if (isEventPending($Authentication::TransactionCompletionSchedule)) + cancel($Authentication::TransactionCompletionSchedule); + $Authentication::TransactionCompletionSchedule = schedule(700, 0, Authentication_transactionComplete); + + if ($Authentication::Status::ActiveMode != 0) + { + $Authentication::Buffer[$Authentication::Status::ActiveMode] = $Authentication::Buffer[$Authentication::Status::ActiveMode] @ "\n" @ %line; + } +} + +// connection complete... send the buffer +function AuthenticationInterface::onConnected(%this) +{ + %this.send(%this.data); +} + +function Authentication_transactionComplete() +{ + // terminate the connection + AuthenticationInterface.disconnect(); + + %buffer = trim($Authentication::Buffer[$Authentication::Status::ActiveMode]); + if ($Authentication::Status::ActiveMode == $Authentication::Mode::Available) + { + if (strlen(%buffer) > 0 && %buffer $= "AVAIL") + { + echo("Authentication: Server is available."); + $Authentication::Status::Available = 1; + } + else + { + error("Authentication: Server is not available."); + $Authentication::Status::Available = 0; + } + } + else if ($Authentication::Status::ActiveMode == $Authentication::Mode::Name) + { + if (%buffer $= "TOOSHORT") + { + $Authentication::Status::Name = "Requested name is too short."; + error("Authentication: " @ $Authentication::Status::Name); + + } + else if (%buffer $= "TOOLONG") + { + $Authentication::Status::Name = "Requested name is too long."; + error("Authentication: " @ $Authentication::Status::Name); + } + else if (%buffer $= "INVALID") + { + $Authentication::Status::Name = "Requested name is rejected."; + error("Authentication: " @ $Authentication::Status::Name); + } + else if (%buffer $= "TAKEN") + { + $Authentication::Status::Name = "Requested name is taken."; + error("Authentication: " @ $Authentication::Status::Name); + } + else if (%buffer $= "SUCCESS") + { + $Authentication::Status::Name = "Name is available and acceptable."; + echo("Authentication: " @ $Authentication::Status::Name); + } + else + { + // this shouldn't happen + $Authentication::Status::Name = "Unknown name status code returned from server."; + error("Authentication: " @ $Authentication::Status::Name); + } + } + else if ($Authentication::Status::ActiveMode == $Authentication::Mode::Recover) + { + if (%buffer $= "RECOVERERROR") + { + // this generic error happens if a malformed request is sent to the server + error("Authentication: Unknown credential recovery status code returned from server."); + } + else if (%buffer $= "NOTFOUND") + { + error("Authentication: No user with that name exists."); + } + else if (%buffer $= "INVALIDPASSWORD") + { + error("Authentication: Invalid password provided for that user."); + } + else if (getWord(%buffer, 0) $= "CERT:") + { + %cert = getSubStr(%buffer, 0, strstr(%buffer, "\n")); + %buffer = getSubStr(%buffer, strstr(%buffer, "\n") + 1, strlen(%buffer)); + %exp = getSubStr(%buffer, 0, (strstr(%buffer, "\n") == -1 ? strlen(%buffer) : strstr(%buffer, "\n"))); + + $Authentication::Status::LastCert = %cert; + $Authentication::Status::LastExp = %exp; + echo("Authentication: Successfully downloaded certificate and encrypted key."); + } + else + { + error("Authentication: Unknown recovery status code returned from server."); + } + } + else if ($Authentication::Status::ActiveMode == $Authentication::Mode::Sign) + { + if (%buffer $= "REJECTED") + { + // this is returned if the user created an account from this IP in the last week, or 5 accounts total + $Authentication::Status::Signature = "Server chose to reject account generation request."; + error("Authentication: " @ $Authentication::Status::Signature); + } + else if (%buffer $= "INVALIDNAME") + { + // name taken, or otherwise not allowed + $Authentication::Status::Signature = "Server rejected account name."; + error("Authentication: " @ $Authentication::Status::Signature); + } + else if (%buffer $= "SIGNERROR") + { + $Authentication::Status::Signature = "Corrupt signature request rejected."; + error("Authentication: " @ $Authentication::Status::Signature); + } + else if (strlen(%buffer) > 0 && getFieldCount(%buffer) > 4) + { + %cert = %buffer; + $Authentication::Status::LastCert = %cert; + $Authentication::Status::Signature = "Account generation successful."; + echo("Authentication: " @ $Authentication::Status::Signature); + } + else + { + $Authentication::Status::Signature = "Unknown signature status code returned from server."; + error("Authentication: " @ $Authentication::Status::Signature); + } + } + + // clear out the buffer + $Authentication::Buffer[$Authentication::Status::ActiveMode] = ""; + $Authentication::Status::ActiveMode = 0; +} + +// determine if the server is available +function Authentication_checkAvail() +{ + if ($Authentication::Status::ActiveMode != 0) + { + // already a request active, retry this one in 10 seconds + schedule(10000, 0, Authentication_checkAvail); + return; + } + + $Authentication::Status::ActiveMode = $Authentication::Mode::Available; + + if (isObject(AuthenticationInterface)) + AuthenticationInterface.delete(); + new TCPObject(AuthenticationInterface); + + AuthenticationInterface.data = "AVAIL\n"; + AuthenticationInterface.connect($AuthServer::Address); + $Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete); +} + +// determine if the given name is acceptable/available +function Authentication_checkName(%name) +{ + if ($Authentication::Status::ActiveMode != 0) + { + // already a request active, retry this one in 10 seconds + schedule(10000, 0, Authentication_checkName, %name); + return; + } + + $Authentication::Status::ActiveMode = $Authentication::Mode::Name; + + if (isObject(AuthenticationInterface)) + AuthenticationInterface.delete(); + new TCPObject(AuthenticationInterface); + + AuthenticationInterface.data = "NAME\t" @ %name @ "\n"; + AuthenticationInterface.connect($AuthServer::Address); + $Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete); +} + +// request a certificate and encrypted exponent from the authentication server +function Authentication_recoverAccount(%payload) +{ + if ($Authentication::Status::ActiveMode != 0) + { + // already a request active, retry this one in 10 seconds + schedule(10000, 0, Authentication_recoverAccount, %payload); + return; + } + + $Authentication::Status::ActiveMode = $Authentication::Mode::Recover; + + if (isObject(AuthenticationInterface)) + AuthenticationInterface.delete(); + new TCPObject(AuthenticationInterface); + + AuthenticationInterface.data = "RECOVER\t" @ %payload @ "\n"; + AuthenticationInterface.connect($AuthServer::Address); + $Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete); +} + +// request a new account certificate +function Authentication_registerAccount(%payload) +{ + if ($Authentication::Status::ActiveMode != 0) + { + // already a request active, retry this one in 10 seconds + schedule(10000, 0, Authentication_registerAccount, %payload); + return; + } + + $Authentication::Status::ActiveMode = $Authentication::Mode::Sign; + + if (isObject(AuthenticationInterface)) + AuthenticationInterface.delete(); + new TCPObject(AuthenticationInterface); + + AuthenticationInterface.data = "SIGN\t" @ %payload @ "\n"; + AuthenticationInterface.connect($AuthServer::Address); + $Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete); +} \ No newline at end of file diff --git a/t2csri/autoupdate.cs b/t2csri/autoupdate.cs new file mode 100644 index 0000000..fac7bd0 --- /dev/null +++ b/t2csri/autoupdate.cs @@ -0,0 +1,111 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Bare Bones Auto Update System Version 1.0: 11/06/2008 + +function authConnect_findAutoUpdater() +{ + if ($AutoUpdater::Address !$= "") + return; + + if (isObject(AutoUpdateConnection)) + { + AutoUpdateConnection.disconnect(); + AutoUpdateConnection.delete(); + } + new TCPObject(AutoUpdateConnection); + + %data = "GET /update HTTP/1.1\r\nHost: www.tribesnext.com\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + AutoUpdateConnection.connect("www.tribesnext.com:80"); + AutoUpdateConnection.schedule(1000, send, %data); +} + +function AutoUpdateConnection::onLine(%this, %line) +{ + if (!$AutoUpdater::UpdateFound) + { + $AutoUpdater::Address = %line; + %this.disconnect(); + autoUpdate_verifyLookup(); + } + else + { + if (isEventPending($AutoUpdate::LastLineSch)) + cancel($AutoUpdate::LastLineSch); + $AutoUpdate::LastLineSch = autoUpdate_applyUpdate(); + if ($AutoUpdate::UpdateStarted) + $AutoUpdate::Buffer = $AutoUpdate::Buffer @ "\n" @ %line; + else if (strlen(%line) == 0) + $AutoUpdate::UpdateStarted = 1; + } +} + +function autoUpdate_verifyLookup() +{ + if (getFieldCount($AutoUpdate::Address) != 2) + { + $AutoUpdater::Address = ""; + error("No valid update address found."); + return; + } + %address = getField($AutoUpdater::Address, 0); + %signature = getField($AutoUpdater::Address, 1); + + %sha1sum = sha1sum(%address); + if (%sha1sum !$= t2csri_verify_update_signature(%signature)) + { + // signature verification failed... someone has subverted the auth server lookup + error("Auto update lookup returned an address with an invalid signature."); + error("Unable to download update without a correct signature."); + $AutoUpdater::Address = ""; + return; + } + else + { + echo("New update found at " @ %address @ ". Ready to download."); + $AutoUpdater::Address = %address; + $AutoUpdater::UpdateFound = 1; + } +} + +// perform signature verification to prove that the update server has designated the +// provided URL for a download, we don't want people injecting arbitrary code into +// user installations +function t2csri_verify_update_signature(%sig) +{ + rubyEval("tsEval '$temp=\"' + t2csri_verify_update_signature('" @ %sig @ "') + '\";'"); + return $temp; +} + +function autoUpdate_performUpdate() +{ + if ($AutoUpdater::Address $= "") + return; + + if (isObject(AutoUpdateConnection)) + { + AutoUpdateConnection.disconnect(); + AutoUpdateConnection.delete(); + } + new TCPObject(AutoUpdateConnection); + + %host = getSubStr($AutoUpdater::Address, 0, strstr("/")); + %uri = getSubStr($AutoUpdater::Address, strlen(%host), strlen($AutoUpdater::Address)); + + %data = "GET " @ %uri @ " HTTP/1.1\nHost: " @ %host @ "\nUser-Agent: Tribes 2\nConnection: close\n\n"; + AutoUpdateConnection.connect(%host); + AutoUpdateConnection.schedule(1000, send, %data); +} + +function autoUpdate_applyUpdate() +{ + new FileObject(AutoUpdateFile); + AutoUpdateFile.openForWrite("autoUpdate.rb"); + AutoUpdateFile.writeline($AutoUpdate::Buffer); + AutoUpdateFile.close(); + AutoUpdateFile.delete(); + + rubyExec("autoUpdate.rb"); +} diff --git a/t2csri/bans.cs b/t2csri/bans.cs new file mode 100644 index 0000000..f218416 --- /dev/null +++ b/t2csri/bans.cs @@ -0,0 +1,93 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// IP and GUID ban list handling. +// These seem to be completely broken in engine, so... here is a script implementation. + +// Still works the same way as before... so scripts will function unmodified. +// BanList::add( %guid, %ipAddress, %seconds); +// If both GUID and IP address are specified, both types of entries are made on the banlist. + +// gets the current Unix Epoch time from Ruby -- in seconds +function currentEpochTime() +{ + rubyEval("tsEval '$temp=' + Time.now.to_i.to_s + ';'"); + return $temp; +} + +// compute the addition in Ruby, due to the Torque script precision problems for >1e6 values +function getEpochOffset(%seconds) +{ + rubyEval("tsEval '$temp=' + (Time.now.to_i + " @ %seconds @ ").to_s + ';'"); + return $temp; +} + +// bans are added to the $BanList::GUID and $BanList::IP hash maps as the Unix epoch time +// when the ban will expire +function BanList::add(%guid, %ipAddress, %seconds) +{ + if (%guid != 0) + { + // add GUID ban + $BanList::GUID[%guid] = getEpochOffset(%seconds); + } + if (getSubStr(%ipAddress, 0, 3) $= "IP:") + { + // add IP ban + %bareIP = getSubStr(%ipAddress, 3, strLen(%ipAddress)); + %bareIP = getSubStr(%bareIP, 0, strstr(%bareIP, ":")); + %bareIP = strReplace(%bareIP, ".", "_"); // variable access bug workaround + + $BanList::IP[%bareIP] = getEpochOffset(%seconds); + } + + // write out the updated bans to the file + export("$BanList*", "prefs/banlist.cs"); +} + +// returns boolean on whether the given client is IP banned or not +// true if banned, false if not banned +function banList_checkIP(%client) +{ + %ip = %client.getAddress(); + %ip = getSubStr(%ip, 3, strLen(%ip)); + %ip = getSubStr(%ip, 0, strstr(%ip, ":")); + %ip = strReplace(%ip, ".", "_"); + + %time = $BanList::IP[%ip]; + if (%time !$= "") + { + //%delta = %time - currentEpochTime(); + // T2 arithmetic fail again... doing subtraction in Ruby + rubyEval("tsEval '$temp=' + (" @ %time @ " - Time.now.to_i).to_s + ';'"); + %delta = $temp; + + if (%delta > 0) + return 1; + else + deleteVariables("$BanList::IP" @ %ip); + } + return 0; +} + +// returns boolean on whether the given GUID is banned or not +// true if banned, false if not banned +function banList_checkGUID(%guid) +{ + %time = $BanList::GUID[%guid]; + if (%time !$= "") + { + //%delta = %time - currentEpochTime(); + // T2 arithmetic fail again... doing subtraction in Ruby + rubyEval("tsEval '$temp=' + (" @ %time @ " - Time.now.to_i).to_s + ';'"); + %delta = $temp; + + if (%delta > 0) + return 1; + else + deleteVariables("$BanList::GUID" @ %guid); + } + return 0; +} \ No newline at end of file diff --git a/t2csri/base64.cs b/t2csri/base64.cs new file mode 100644 index 0000000..d4f86b7 --- /dev/null +++ b/t2csri/base64.cs @@ -0,0 +1,164 @@ +// Torque Script Base64 Utilities +// Written by Electricutioner +// 10:43 PM 7/13/2005 + +// Used under license by the Tribes 2 Community System Re-engineering Intitiative. +// License Granted: 10/31/2008 + +// necessary for the transfer of arbitrary binary data over ASCII connections +function Base64_Encode(%string) +{ + %encoded = ""; + for (%i = 0; %i < strLen(%string); %i += 3) + { + %binBlock = ""; + for (%j = 0; %j < 3; %j++) + { + %bin = DecToBin(strCmp(getSubStr(%string, %i + %j, 1), "")); + while (strLen(%bin) < 8 && strLen(%bin) != 0) + %bin = "0" @ %bin; + %binBlock = %binBlock @ %bin; + } + for (%j = 0; %j < 4; %j++) + { + %bin = getSubStr(%binBlock, 6 * %j, 6); + if (%bin !$= "") + { + while(strLen(%bin) < 6) + %bin = %bin @ "0"; + %encoded = %encoded @ $Base64Utils::Base64Chars[BinToDec(%bin)]; + } + else + %encoded = %encoded @ "="; + } + } + return %encoded; +} +function Base64_Decode(%string) +{ + %decoded = ""; + for (%i = 0; %i < strLen(%string); %i += 4) + { + %binBlock = ""; + for (%j = 0; %j < 4; %j++) + { + %bin = ""; + %val = Base64_ValToIndex(strCmp(getSubStr(%string, %i + %j, 1), "")); + if (%val != -1) + %bin = DecToBin(%val); + while (strLen(%bin) < 6 && %val != -1) + %bin = "0" @ %bin; + %binBlock = %binBlock @ %bin; + } + for (%j = 0; %j < 3; %j++) + { + %bin = getSubStr(%binBlock, 8 * %j, 8); + while(strLen(%bin) < 8 && strLen(%bin) != 0) + %bin = "0" @ %bin; + if (%bin !$= "") + %decoded = %decoded @ collapseEscape("\\x" @ DecToHex(BinToDec(%bin))); + } + } + + return %decoded; +} +// a few conditionals are better than a loop +function Base64_ValToIndex(%val) +{ + if (%val > 96 && %val < 123) + return %val - 71; + else if (%val > 64 && %val < 91) + return %val - 65; + else if (%val > 47 && %val < 58) + return %val + 4; + else if (%val == 43) + return 62; + else if (%val == 47) + return 63; + else if (%val == 61) + return -1; + else + return ""; +} + +//create the character array in a minimum of fuss +function Base64_CreateArray() +{ + for (%i = 0; %i < 26; %i++) + { + $Base64Utils::Base64Chars[%i] = collapseEscape("\\x" @ DecToHex(65 + %i)); + $Base64Utils::Base64Chars[%i + 26] = collapseEscape("\\x" @ DecToHex(97 + %i)); + + if (%i < 10) + $Base64Utils::Base64Chars[%i + 52] = %i; + } + $Base64Utils::Base64Chars[62] = "+"; + $Base64Utils::Base64Chars[63] = "/"; +} + +// these binary conversion functions are much better than older ones +// these can handle just about any size of input, unlike 8 bit like the previous ones +function DecToBin(%dec) +{ + %length = mCeil(mLog(%dec) / mLog(2)); + %bin = ""; + for (%i = 0; %i <= %length; %i++) + { + %test = mPow(2, %length - %i); + if (%dec >= %test) + { + %bin = %bin @ "1"; + %dec -= %test; + } + else if (%i > 0) + %bin = %bin @ "0"; + } + return %bin; +} +function BinToDec(%bin) +{ + %dec = 0; + for (%i = 0; %i < strLen(%bin); %i++) + %dec += getSubStr(%bin, %i, 1) * mPow(2, strLen(%bin) - %i - 1); + return %dec; +} + +//no length limit +function DecToHex(%dec) +{ + %bin = DecToBin(%dec); + while (strLen(%bin) % 4 != 0) + %bin = "0" @ %bin; + + for (%i = 0; %i < strLen(%bin); %i += 4) + { + %block = getSubStr(%bin, strLen(%bin) - %i - 4, 4); + %part = BinToDec(%block); + if (%part > 9) + { + switch (%part) + { + case 10: + %hex = "a" @ %hex; + case 11: + %hex = "b" @ %hex; + case 12: + %hex = "c" @ %hex; + case 13: + %hex = "d" @ %hex; + case 14: + %hex = "e" @ %hex; + case 15: + %hex = "f" @ %hex; + } + } + else + %hex = %part @ %hex; + } + if (strlen(%hex) == 0) + return "00"; + else + return %hex; +} + +Base64_CreateArray(); \ No newline at end of file diff --git a/t2csri/certstore.rb b/t2csri/certstore.rb new file mode 100644 index 0000000..24a8573 --- /dev/null +++ b/t2csri/certstore.rb @@ -0,0 +1,43 @@ +# +# Tribes 2 Community System Reengineering Initiative +# Client Side Credential/Certificate Store +# Version 1.1 (2009/01/25) +# +# Written by Electricutioner/Thyth +# http://absolous.no-ip.com/ +# Copyright 2008 - 2009 +# +# Released under the terms of the GNU General Public License v3 or later. +# http://www.gnu.org/licenses/gpl.html +# Your use of this software is subject to the terms of that license. Use, modification, or distribution +# constitutes acceptance of these software terms. This license is the only manner by which you are permitted +# to use this software, thus rejection of the license terms prohibits your use of this software. +# +$accCerts = Hash.new +$accPrivateKeys = Hash.new + +def certstore_loadAccounts + IO.foreach('public.store') {|line| $accCerts[line.split("\t")[0].downcase] = line.rstrip.lstrip } + IO.foreach('private.store') {|line| $accPrivateKeys[line.split("\t")[0].downcase] = line.rstrip.lstrip } +end + +def certstore_addAccount(public, private) + $accCerts[public.split("\t")[0].downcase] = public + $accPrivateKeys[public.split("\t")[0].downcase] = private + + publicstore = File.new('public.store', 'a') + publicstore.seek(0, IO::SEEK_END) + publicstore.puts(public + "\r\n") + publicstore.close + + privatestore = File.new('private.store', 'a') + privatestore.seek(0, IO::SEEK_END) + privatestore.puts(private + "\r\n") + privatestore.close +end + +def certstore_listAccounts + list = String.new + $accCerts.each_key { |username| list = list.rstrip + "\t" + $accCerts[username].split("\t")[0].to_s } + return list.lstrip +end diff --git a/t2csri/clientSide.cs b/t2csri/clientSide.cs new file mode 100644 index 0000000..0bbb6fc --- /dev/null +++ b/t2csri/clientSide.cs @@ -0,0 +1,391 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 1.1: 03/14/2009 + +// load the clan support functions +exec("t2csri/clientSideClans.cs"); + +// initialize the SHA1 digester in Ruby +function t2csri_initDigester() +{ + $SHA1::Initialized = 1; + rubyEval("$sha1hasher = SHA1Pure.new"); +} + +// use Ruby to get the SHA1 hash of the string +function sha1sum(%string) +{ + if (!$SHA1::Initialized) + t2csri_initDigester(); + %string = strReplace(%string, "'", "\\'"); + rubyEval("$sha1hasher.prepare"); + rubyEval("$sha1hasher.append('" @ %string @ "')"); + rubyEval("tsEval '$temp=\"' + $sha1hasher.hexdigest + '\";'"); + %temp = $temp; + $temp = ""; + return %temp; +} + +// get the password encrypted private key for the following name +// assuming it is installed on the system +function t2csri_getEncryptedAccountKey(%name) +{ + return rubyGetValue("$accPrivateKeys['" @ strlwr(%name) @ "']"); +} + +// get the public certificate key for the following name +// assuming it is installed on the system +function t2csri_getAccountCertificate(%name) +{ + // check if the name exists + %found = 0; + for (%i = 0; %i < getFieldCount($accountList); %i++) + { + if (%name $= getField($accountList, %i)) + %found = 1; + } + + // this is a bit of a hack -- Ruby 1.9.0 has some problems getting the account on the first try + %value = ""; + if (%found) + { + while (strLen(%value) == 0) + { + %value = rubyGetValue("$accCerts['" @ strlwr(%name) @ "']"); + } + } + else + { + %value = rubyGetValue("$accCerts['" @ strlwr(%name) @ "']"); + } + return %value; +} + +// prevents a warning generated when leaving a server, and allows the yellow +// highlight selection on the warrior screen that indicates the active account +function WONGetAuthInfo() +{ + return getField($LoginCertificate, 0) @ "\t\t0\t" @ getField($LoginCertificate, 1) @ "\n"; +} + +// decrypt an RC4 encrypted account key +// also used for encryption on the plaintext when generating the account +function t2csri_decryptAccountKey(%account, %password, %nonce, %doingEncryption) +{ + %key = sha1sum(%password @ %nonce); + + // initiate RC4 stream state with key + %iterations = 256; + for (%i = 0; %i < %iterations; %i++) + { + %SArray[%i] = %i; + } + %j = 0; + for (%i = 0; %i < %iterations; %i++) + { + %j = (%j + %SArray[%i] + strCmp(getSubStr(%key, %i % strLen(%key), 1), "")) % %iterations; + + //swap(S[i],S[j]) + %temp = %SArray[%i]; + %SArray[%i] = %SArray[%j]; + %SArray[%j] = %temp; + } + + // discard 2048 bytes from the start of the stream to avoid the strongly biased first bytes + %seedI = 0; %seedJ = 0; + for (%i = 0; %i < 2048; %i++) + { + %seedI = (%seedI + 1) % 256; + %seedJ = (%seedJ + %SArray[%seedI]) % 256; + + %temp = %SArray[%seedI]; + %SArray[%seedI] = %SArray[%seedJ]; + %SArray[%seedJ] = %temp; + } + + // decrypt the account + %bytes = strlen(%account) / 2; + for (%i = 0; %i < %bytes; %i++) + { + %seedI = (%seedI + 1) % 256; + %seedJ = (%seedJ + %SArray[%seedI]) % 256; + + %temp = %SArray[%seedI]; + %SArray[%seedI] = %SArray[%seedJ]; + %SArray[%seedJ] = %temp; + + %schar = %SArray[(%SArray[%seedI] + %SArray[%seedJ]) % 256]; + %achar = strCmp(collapseEscape("\\x" @ getSubStr(%account, %i * 2, 2)), ""); + %byte = DecToHex(%schar ^ %achar); + if (strLen(%byte) < 2) + %byte = "0" @ %byte; + %out = %out @ %byte; + } + + // verify that the password is correct by checking with the nonce (SHA1 plaintext hash) + %hash = sha1sum(%out); + if (%hash $= %nonce || %doingEncryption) + return %out; + else + { + %out = getSubStr(%out, 0, strlen(%out) - 2); + // last 4-bit block was corrupted... try to fix it + for (%i = 0; %i < 16; %i++) + { + %chunk = getSubStr(DecToHex(%i), 1, 1); + %hash = sha1sum(%out @ %chunk); + if (%hash $= %nonce) + return %out @ %chunk; + } + // last 8-bit block was corrupted... try to fix it + for (%i = 0; %i < 256; %i++) + { + %chunk = DecToHex(%i); + %hash = sha1sum(%out @ %chunk); + if (%hash $= %nonce) + return %out @ %chunk; + } + + // looks like the password was still wrong + return ""; + } +} + +function t2csri_encryptAccountKey(%account, %password) +{ + %nonce = sha1sum(%account); + return %nonce @ ":" @ t2csri_decryptAccountKey(%account, %password, %nonce, 1); +} + +// this does the "login" process internally for accounts that exist +// it finds the cert, the private key, decrypts it, and sets up the +// RSA key data structures in the Ruby environment. +function t2csri_getAccount(%username, %password) +{ + $LoginUsername = %username; + $LoginCertificate = t2csri_getAccountCertificate(%username); + if ($LoginCertificate $= "") + { + return "NO_SUCH_ACCOUNT"; + } + + // split the certificate into its components + // username guid e n signature + %user = getField($LoginCertificate, 0); + %guid = getField($LoginCertificate, 1); + %e = getField($LoginCertificate, 2); + %n = getField($LoginCertificate, 3); + %sig = getField($LoginCertificate, 4); + + // nonce:encrypted + %encryptedKey = t2csri_getEncryptedAccountKey(%username); + %encryptedKey = getField(%encryptedKey, 1); // strip the username from the field + %nonce = getSubStr(%encryptedKey, 0, strstr(%encryptedKey, ":")); + %block = getSubStr(%encryptedKey, strLen(%nonce) + 1, strLen(%encryptedKey)); + %decryptedKey = t2csri_decryptAccountKey(%block, %password, %nonce); + if (%decryptedKey $= "") + { + return "INVALID_PASSWORD"; + } + + // we have the account, and the properly decrypted private key... interface with Ruby and + // insert the data... + rubyEval("$accountKey = RSAKey.new"); + rubyEval("$accountKey.e = '" @ %e @ "'.to_i(16)"); + rubyEval("$accountKey.n = '" @ %n @ "'.to_i(16)"); + rubyEval("$accountKey.d = '" @ %decryptedKey @ "'.to_i(16)"); + // protect the private exponent (d) from reading now. + // this will prevent scripts from stealing the private exponent, but still + // allows doing decryption using the player's account key + rubyEval("$accountKey.protect"); + + return "SUCCESS"; +} + +// this sends a request to the authentication server to retrieve an account that is +// not locally stored on the client machine. It does some fancy mangling on the +// password to prevent the authentication server from decrypting the password +function t2csri_downloadAccount(%username, %password) +{ + // clear out any previously downloaded account + $Authentication::Status::LastCert = ""; + $Authentication::Status::LastExp = ""; + + // bring up a UI to indicate account download is in progress + LoginMessagePopup("DOWNLOADING", "Downloading account credentials..."); + + // this hash is what the auth server stores -- it does not store the password + // in a recoverable manner + %authStored = sha1sum("3.14159265" @ strlwr(%username) @ %password); + //echo(%authStored); + + // get time in UTC, use it as a nonce to prevent replay attacks + rubyEval("tsEval '$temp=\"' + Time.new.getutc.to_s + '\";'"); + %utc = $temp; + $temp = ""; + //echo(%utc); + + // time/username nonce + %timeNonce = sha1sum(%utc @ strlwr(%username)); + //echo(%timeNonce); + + // combined hash + %requestHash = sha1sum(%authStored @ %timeNonce); + //echo(%requestHash); + + // sent to server: username utc requesthash + // server sends back: certificate and encrypted private exponent + Authentication_recoverAccount(%username @ "\t" @ %utc @ "\t" @ %requestHash); + t2csri_processDownloadCompletion(); +} + +function t2csri_processDownloadCompletion() +{ + if ($Authentication::Status::ActiveMode != 0) + { + schedule(128, 0, t2csri_processDownloadCompletion); + return; + } + else + { + if (strlen($Authentication::Status::LastCert) > 0) + { + popLoginMessage(); + LoginMessagePopup("SUCCESS", "Account credentials downloaded successfully."); + schedule(3000, 0, popLoginMessage); + + %cert = strreplace($Authentication::Status::LastCert, "'", "\\'"); + %exp = strreplace($Authentication::Status::LastExp, "'", "\\'"); + %cert = getSubStr(%cert, 6, strlen(%cert)); + %exp = getField(%cert, 0) @ "\t" @ getSubStr(%exp, 5, strlen(%exp)); + // add it to the store + rubyEval("certstore_addAccount('" @ %cert @ "','" @ %exp @ "')"); + + // refresh the UI + $LastLoginKey = $LoginName; + LoginEditMenu.clear(); + LoginEditMenu.populate(); + LoginEditMenu.setActive(1); + LoginEditMenu.setSelected(0); + LoginEditBox.clear(); + } + else + { + popLoginMessage(); + if ($Authentication::RecoveryError $= "") + { + $Authentication::RecoveryError = "The server did not respond [a firewall may cause this]."; + } + LoginMessagePopup("ERROR", "Credential download failed: " @ $Authentication::RecoveryError); + schedule(3000, 0, popLoginMessage); + } + } +} + +// gets a hex version of the game server's IP address +// used to prevent a replay attack as described by Rain +function t2csri_gameServerHexAddress() +{ + %ip = ServerConnection.getAddress(); + %ip = getSubStr(%ip, strstr(%ip, ":") + 1, strlen(%ip)); + %ip = getSubStr(%ip, 0, strstr(%ip, ":")); + %ip = strReplace(%ip, ".", " "); + + for (%i = 0; %i < getWordCount(%ip); %i++) + { + %byte = DecToHex(getWord(%ip, %i)); + if (strLen(%byte) < 2) + %byte = "0" @ %byte; + %hex = %hex @ %byte; + } + return %hex; +} + +// client side interface to communicate with the game server +function clientCmdt2csri_pokeClient(%version) +{ + echo("T2CSRI: Authenticating with connected game server."); + + // send the community certificate, assuming server is running later than 1.0 + if (getWord(%version, 1) > 1.0) + t2csri_sendCommunityCert(); + + $encryptedchallenge = ""; + + // send the certificate in 200 byte parts + for (%i = 0; %i < strlen($LoginCertificate); %i += 200) + { + commandToServer('t2csri_sendCertChunk', getSubStr($LoginCertificate, %i, 200)); + } + + // send a 64 bit challenge to the server to prevent replay attacks + rubyEval("tsEval '$loginchallenge=\"' + rand(18446744073709551615).to_s(16) + '\";'"); + // append what the client thinks the server IP address is, for anti-replay purposes + $loginchallenge = $loginchallenge @ t2csri_gameServerHexAddress(); + + // wait a second to make sure the com cert data is transferred + schedule(1000, 0, commandToServer, 't2csri_sendChallenge', $loginchallenge); + + // at this point, server will validate the signature on the certificate then + // proceed to verifying the client has the private part of the key if valid + // or disconnecting them if invalid + // the only way the client can have a valid cert is if the auth server signed it +} + +function clientCmdt2csri_getChallengeChunk(%chunk) +{ + $encryptedchallenge = $encryptedchallenge @ %chunk; +} + +function clientCmdt2csri_decryptChallenge() +{ + // sanitize the challenge to make sure it contains nothing but hex characters. + // anything else means that the server is trying to hijack control of the interpreter + %challenge = strlwr($encryptedchallenge); + for (%i = 0; %i < strlen(%challenge); %i++) + { + %char = strcmp(getSubStr(%challenge, %i, 1), ""); + if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97)) + { + schedule(1000, 0, MessageBoxOK, "REJECTED","Invalid characters in server challenge."); + disconnect(); + return; + } + } + + rubyEval("tsEval '$decryptedChallenge=\"' + $accountKey.decrypt('" @ %challenge @ "'.to_i(16)).to_s(16) + '\";'"); + + // verify that the client challenge is intact, and extract the server challenge + %replayedClientChallenge = getSubStr($decryptedChallenge, 0, strLen($loginchallenge)); + %serverChallenge = getSubStr($decryptedChallenge, strlen(%replayedClientChallenge), strLen($decryptedChallenge)); + if (%replayedClientChallenge !$= $loginchallenge) + { + schedule(1000, 0, MessageBoxOK, "REJECTED","Server sent back wrong client challenge."); + disconnect(); + return; + } + + // analyze the IP address the server thinks the client is connecting from for the purposes + // of preventing replay attacks + %clip = ipv4_hexBlockToIP(getSubStr(%serverChallenge, strLen(%serverChallenge) - 8, 8)); + if (!ipv4_reasonableConnection(ipv4_hexBlockToIP(t2csri_gameServerHexAddress()), %clip)) + { + schedule(1000, 0, MessageBoxOK, "REJECTED","Server sent back unreasonable IP challenge source. Possible replay attack attempt."); + disconnect(); + return; + } + + // send the server part of the challenge to prove client identity + // this is done on a schedule to prevent side-channel timing attacks on the client's + // private exponent -- different x requires different time for x^d, and d bits can be found + // if you are really resourceful... adding this schedule kills time accuracy and makes such + // a correlation attack very improbable + schedule(getRandom(128, 512), 0, commandToServer, 't2csri_challengeResponse', %serverChallenge); + + // at this point, server will verify that the challenge is equivalent to the one it sent encrypted + // to the client. the only way it can be equivalent is if the client has the private key they + // claim to have. normal T2 connection process continues from this point +} diff --git a/t2csri/clientSideClans.cs b/t2csri/clientSideClans.cs new file mode 100644 index 0000000..26a90e9 --- /dev/null +++ b/t2csri/clientSideClans.cs @@ -0,0 +1,54 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 0.5: 2009-03-18 + +// A little bit of development theory: +// -The Apotheosis DLL contains 3 RSA public keys. One for authentication, one for updates, +// and one for delegation. The delegation key forms the root of the community system trust heirarchy. +// -The delegated-community-enhancement server issues time limited community certificates, which +// annotate the bare account certificates. The annotations include current name, current clan, current tag +// and current clan membership so that getAuthInfo() provides all relevant information. These certificates +// are time limited to enforce the "current" status of the annotations. +// -Since game servers don't communicate with centralized systems (except for listing), the client is +// responsible for providing a signed community certificate, and if prompted, the client is also +// responsible for providing the authoratatively signed certificate from the relevant DCE. Thus, the +// server will accumilate a small cache of valid DCE certificates. + +// DCE certificate format: +// DCEName DCENum IssuedEpoch ExpireEpoch 0 0 e n sig +// The two zeros are reserved for future use. +// Community certificate format: +// DCENum IssuedEpoch ExpireEpoch IssuedForGUID HexBlob Sig +// HexBlob format: +// (Follows same format as contents returned by getAuthInfo, but is hex encoded.) + +function clientCmdt2csri_requestUnknownDCECert(%dceNum) +{ + %cert = $T2CSRI::ClientDCESupport::DCECert[%dceNum]; + if (%cert $= "") + return; // we don't have it, so we can't send it + + %len = strlen(%cert); + for (%i = 0; %i < %len; %i += 200) + { + commandToServer('t2csri_getDCEChunk', getSubStr(%cert, %i, 200)); + } + commandToServer('t2csri_finishedDCE'); +} + +function t2csri_sendCommunityCert() +{ + %cert = $T2CSRI::CommunityCertificate; + if (%cert $= "") + return; // we don't have it, so we can't send it + + %len = strlen(%cert); + for (%i = 0; %i < %len; %i += 200) + { + commandToServer('t2csri_sendCommunityCertChunk', getSubStr(%cert, %i, 200)); + } + commandToServer('t2csri_comCertSendDone'); +} diff --git a/t2csri/community/browser.cs b/t2csri/community/browser.cs new file mode 100644 index 0000000..0493b57 --- /dev/null +++ b/t2csri/community/browser.cs @@ -0,0 +1,1021 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2011-2012 + +// Tribes 2 Community System +// Robot Browser Client + +// This scripts implements a network data interface to the TribseNext community system browser robot data +// interface. The "robot" data interface provides the data in a way that is easy to parse with the meager +// and mediocre string processing and parsing features present in the Tribes 2 game. If you are reading this +// script and desire to make some sort of third party client for web access or other purposes, you will have +// a much easier time if you use the JSON API to access the same data. + +// Currently available methods (as of RC3) are as follow: +// - Retrieval of a "Community Enhanced Certificate" indicating current name/tag/membership. +// - Search for clans by name. +// - Search for players by name. +// - View a clan profile. +// - View a player profile. +// - View a clan history. +// - View a player history. +// - Change recruiting status of a clan. +// - Change the profile info of a clan. +// - Change a clan's tag. +// - Change a clan's website. +// - Change a clan name. +// - Change which in-game picture is displayed on the clan profile page. +// - Invite a player to a clan. +// - View outstanding invites to a clan. +// - Change the rank/title of either one's self, or of others in a clan. +// - Authorize disbanding a clan. +// - Kick another player from a clan. +// - Change the player name of an account. +// - Change which clan tag an account will display. +// - Change a player's profile website. +// - Change a player's profile info. +// - Accept an invitation to join a clan. +// - Reject an invitation to join a clan. +// - Leave a clan. +// - Create a clan. + +$TribesNext::Community::Browser::Active = 0; + +function CommunityBrowserInterface::onConnected(%this) +{ + echo("Browser-Sending: " @ %this.data); + %this.primed = 0; + %this.send(%this.data); +} + +function CommunityBrowserInterface::onDisconnect(%this) +{ + if (!%this.primed) + { + // nothing sent from the server + // this means there is probably a firewall interfering with the communication + // with a rare chance that the browser system is unavailable (in which case we will post an announcement) + // alert the user the first time + if (!$TribesNext::Community::Browser::Firewalled) + { + $TribesNext::Community::Browser::Firewalled = 1; + schedule(500, 0, MessageBoxOK, "NETWORK", "Unable to communicate with the browser server via HTTP. Reconfigure your firewall to allow access."); + } + } + $TribesNext::Community::Browser::Active = 0; + tn_community_Browser_executeNextRequest(); +} + +function CommunityBrowserInterface::onLine(%this, %line) +{ + if (trim(%line) $= "") + { + %this.primed = 1; + return; + } + if (!%this.primed) + return; + + warn("Browser: " @ %line); + + if (getSubStr(%line, 0, 4) $= "ERR:") + { + // A really exceptional error happened in the browser system + schedule(500, 0, MessageBoxOK, "SYSTEM ERROR", trim(getSubStr(%line, 4, strlen(%line)))); + } + + %message = getField(%line, 0); + switch$ (%message) + { + // display errors to the user -- most of these should never actually happen + // unless the user is being intentionally naughty or is operating on stale data + case "ERR": + if (getField(%line, 1) $= "BROWSER") + { + schedule(500, 0, MessageBoxOK, "ERROR", getFields(%line, 2)); + } + + // DCE and CEC returns are certificates that are handed to game servers to get tags + // and handle name changes -- they replace the authInfo field, instead of having the + // server generate a skeleton version of the authInfo from the GUID and name. + // in the absense of a DCE/CEC pair, the player will still be able to play with the + // "raw" account, using the original account name and no tag + case "DCE": + %dceCert = collapseEscape(getField(%line, 1)); + %index = getField(%dceCert, 1); + $T2CSRI::ClientDCESupport::DCECert[%index] = %dceCert; + case "CEC": + $T2CSRI::CommunityCertificate = collapseEscape(getField(%line, 1)); + // schedule a refresh + %expire = getField($T2CSRI::CommunityCertificate, 2); + rubyEval("tsEval '$temp=\"' + (" @ %expire @ " - Time.now().to_i).to_s + '\";'"); + %expire = $temp - 60; + if (%expire > 0) + { + if (isEventPending($TribesNext::Browser::CertRefreshSch)) + cancel($TribesNext::Browser::CertRefreshSch); + $TribesNext::Browser::CertRefreshSch = schedule(1000 * %expire, 0, tn_community_Browser_request_cert); + } + else + { + schedule(500, 0, MessageBoxOK, "ERROR", "Received expired certificate from community server. Is your computer's clock set correctly?"); + } + + // data access methods + + // results for searching for a clan by name + case "CSEARCH": + %query = getField(%line, 1); + if ($Browser::CQuery !$= %query) + { + // new query -- wipe old results + deleteVariables("$Browser::CResults*"); + $Browser::CQuery = %query; + $Browser::CCount = ""; + } + + %idx = getField(%line, 2) + 0; + %answer = getFields(%line, 3); + $Browser::CResults[%idx] = %answer; + if (%idx >= $Browser::CCount) + $Browser::CCount = %idx; + + // results for querying a clan (all necessary to display in UI) + case "CLAN": + %id = getField(%line, 2); + %var = getField(%line, 1); + + %clan = tn_community_browser_getClanProfile(%id); + if (%var $= "NAME") + { + // wipe the players so membership doesn't look screwed up if it shrinks + tn_community_browser_wipePlayers(%clan); + %clan.name = getField(%line, 3); + %clan.lastRefresh = getSimTime(); + } + else if (%var $= "TAG") + { + %clan.tag = getField(%line, 3) TAB getField(%line, 4); + } + else if (%var $= "RECR") + { + %clan.recruiting = getField(%line, 3); + } + else if (%var $= "SITE") + { + %clan.site = getField(%line, 3); + } + else if (%var $= "DATE") + { + // convert epoch to human readable + %clan.date = tn_community_mailui_epochToDate(getField(%line, 3)); + } + else if (%var $= "PICT") + { + %clan.picture = getField(%line, 3); + } + else if (%var $= "ACTIVE") + { + %clan.active = getField(%line, 3); + } + else if (%var $= "INFO") + { + %clan.info = collapseEscape(getField(%line, 3)); + } + else if (%var $= "MEMB") + { + %idx = getField(%line, 3) + 0; + %clan.player[%idx] = getFields(%line, 4); + + if (%idx >= %clan.pcount) + %clan.pcount = %idx; + } + + // results for querying a clan history + case "CHIST": + %id = getField(%line, 1); + %idx = getField(%line, 2) + 0; + + %clan = tn_community_browser_getClanProfile(%id); + %event = getFields(%line, 3); + if (%event !$= %clan.history[%idx]) + { + // underlying event has changed, clear the display cache + // this may occur if one of the underlying players has changed + // active clan, or name + %clan.historyCache[%idx] = ""; + } + %clan.history[%idx] = %event; + + if (%idx >= %clan.hcount) + %clan.hcount = %idx; + + // results for searching for a player by name + case "SEARCH": + %query = getField(%line, 1); + if ($Browser::PQuery !$= %query) + { + // new query -- wipe old results + deleteVariables("$Browser::PResults*"); + $Browser::PQuery = %query; + $Browser::PCount = ""; + } + + %idx = getField(%line, 2) + 0; + %answer = tn_community_util_extractPlayer(%line, 3); + $Browser::PResults[%idx] = %answer; + if ($Browser::PCount $= "" || %idx >= $Browser::PCount) + $Browser::PCount = %idx; + + // results for querying a player (all necessary to display in UI) + case "PLAYER": + %guid = getField(%line, 2); + %var = getField(%line, 1); + + %player = tn_community_browser_getPlayerProfile(%guid); + + if (%var $= "NAME") + { + tn_community_browser_wipeMemberships(%player); + %player.name = getField(%line, 3); + %player.lastRefresh = getSimTime(); + } + else if (%var $= "TAG") + { + %player.tag = getField(%line, 3) TAB getField(%line, 4); + } + else if (%var $= "DATE") + { + // convert epoch to human readable + %player.date = tn_community_mailui_epochToDate(getField(%line, 3)); + } + else if (%var $= "SITE") + { + %player.site = getField(%line, 3); + } + else if (%var $= "INFO") + { + %player.info = collapseEscape(getField(%line, 3)); + } + else if (%var $= "ONLINE") + { + %player.online = getField(%line, 3); + } + else if (%var $= "CLAN") + { + %idx = getField(%line, 3) + 0; + %player.membership[%idx] = getFields(%line, 4); + + if (%idx >= %player.mcount) + %player.mcount = %idx; + } + + // results for querying a player history + case "PHIST": + %guid = getField(%line, 1); + %idx = getField(%line, 2) + 0; + + %player = tn_community_browser_getPlayerProfile(%guid); + %event = getFields(%line, 3); + if (%event !$= %player.history[%idx]) + { + // underlying event has changed, clear the display cache + // this may occur if one of the underlying players has changed + // active clan, or name + %player.historyCache[%idx] = ""; + } + %player.history[%idx] = %event; + + if (%idx >= %player.hcount) + %player.hcount = %idx; + + // clan management + + // clan recruit flag changes + case "CLAN_RECRUITING": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 2); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // clan info page changes + case "CLAN_INFO": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 2); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // clan tag changes + case "CLAN_RETAGGED": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 1); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // clan website changes + case "CLAN_WEBSITE": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 1); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // clan renamed + case "CLAN_RENAMED": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 1); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // clan team picture changed + case "CLAN_TEAMPIC": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 2); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // clan invitation sent + case "INVITED": + %id = getField(%line, 2); + %guid = getField(%line, 1); + %clan = tn_community_browser_getClanProfile(%id); + %player = tn_community_browser_getPlayerProfile(%guid); + + if (%clan.name !$= "" && %player.name !$= "") + { + // have both clan and player name + %out = "Sent \"" @ %player.name @ "\" an invitation to join \"" @ %clan.name @ "\"."; + } + else if (%clan.name $= "" && %player.name !$= "") + { + // just have player name + %out = "Sent invitation to \"" @ %player.name @ "\"."; + } + else if (%clan.name !$= "" && %player.name $= "") + { + // just have clan name + %out = "Sent invitation to join \"" @ %clan.name @ "\"."; + } + else + { + // have neither clan or player name + %out = "Invitation sent."; + } + schedule(500, 0, MessageBoxOK, "INVITATION", %out); + + // results for querying the pending invites on a clan + case "INVITEE": + %id = getField(%line, 1); + %idx = getField(%line, 2) + 0; + + %clan = tn_community_browser_getClanProfile(%id); + %clan.invitee[%idx] = getFields(%line, 3); + + if (%idx > %clan.icount) + %clan.icount = %idx; + + + // changing a clan rank/title of one's self + case "CLAN_RANK_SET": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 1); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // changing a clan rank/title of another clan member + case "CLAN_RANK_OTHER": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 1); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // retracting a disband authorization + case "DISBAND_RETRACTED": + %id = getField(%line, 1); + %clan = tn_community_browser_getClanProfile(%id); + if (%clan.name !$= "") + { + %out = "You have retracted your disband authorization for \"" @ %clan.name @ "\"."; + } + else + { + %out = "You have retracted your disband authorization for the clan."; + } + schedule(500, 0, MessageBoxOK, "AUTHORIZATION", %out); + + // clan disband authorized, but still need disband consensus + case "AUTHORIZED_DISBAND": + %id = getField(%line, 1); + %clan = tn_community_browser_getClanProfile(%id); + if (%clan.name !$= "") + { + %out = "You have authorized disbanding \"" @ %clan.name @ "\"."; + } + else + { + %out = "You have authorized disbanding the clan."; + } + schedule(500, 0, MessageBoxOK, "AUTHORIZATION", %out); + + // clan disband authorized and disband completed + case "DISBANDED_CLAN": + %id = getField(%line, 1); + %clan = tn_community_browser_getClanProfile(%id); + if (%clan.name !$= "") + { + %out = "You have disbanded clan \"" @ %clan.name @ "\"."; + } + else + { + %out = "You have disbanded the clan."; + } + + schedule(500, 0, MessageBoxOK, "DISBANDED", %out); + + tn_community_browser_clan_view(%id); + + // kicked another person from clan + case "CLAN_KICKED": + // for these clan management methods, we just queue a redownload of the profile page + %id = getField(%line, 1); + + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + + // user profile management + + // user successfully renamed the visible name of their account + case "RENAMED": + // update our own profile and certificate + tn_community_browser_user_view(getField($LoginCertificate, 1)); + tn_community_browser_request_cert(); + + // user switched their active clan tag (or turned it off) + case "ACTIVE_CLAN": + // update our own profile and certificate + tn_community_browser_user_view(getField($LoginCertificate, 1)); + tn_community_browser_request_cert(); + + // user switched their profile website link + case "PLAYER_WEBSITE": + // update our own profile + tn_community_browser_user_view(getField($LoginCertificate, 1)); + + // user switched their profile info + case "PLAYER_INFO_UPDATED": + // update our own profile + tn_community_browser_user_view(getField($LoginCertificate, 1)); + + // player accepted an invitation to join a clan + case "CLAN_JOINED": + // update our copy of the clan profile + %id = getField(%line, 1); + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + // also update our own profile and certificate + tn_community_browser_user_view(getField($LoginCertificate, 1)); + tn_community_browser_request_cert(); + + // player has rejected an invitation to join a clan + case "INVITATION_DECLINED": + %id = getField(%line, 1); + %clan = tn_community_browser_getClanProfile(%id); + if (%clan.name !$= "") + { + %out = "You have rejected an invitation to join \"" @ %clan.name @ "\"."; + } + else + { + %out = "You have rejected this clan invitation."; + } + + schedule(500, 0, MessageBoxOK, "INVITATION", %out); + + // player has left a clan + case "CLAN_LEFT": + // update our copy of the clan profile + %id = getField(%line, 1); + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + // also update our own profile and certificate + tn_community_browser_user_view(getField($LoginCertificate, 1)); + tn_community_browser_request_cert(); + + // player has created a clan + case "CLAN_CREATED": + // update our copy of the clan profile + %id = getField(%line, 1); + tn_community_browser_getClanProfile(%id).lastRefresh = 0; + tn_community_browser_clan_view(%id); + // also update our own profile and certificate + tn_community_browser_user_view(getField($LoginCertificate, 1)); + tn_community_browser_request_cert(); + + // UI hook: pop the creation dialog + Canvas.popDialog(CreateTribeDlg); + // for the newly created clan, create a new tab switch us to it + LaunchTabView.viewTab("BROWSER", TribeAndWarriorBrowserGui, 0); + TWBTabView.view(%fld[1], "", "Tribe"); + } +} + +// primary data store structures for the browser + +// BrowserPlayer: +// guid (primary key) +// name +// tag = tag \t append +// date (epoch) +// site +// info +// online (boolean) +// mcount +// membership[x] = clanid \t clanname \t rank \t title \t tag \t append +// hcount +// history[x] = (player history entry) -> entry 0 is oldest + +// BrowserClan: +// id (primary key) +// name +// tag = (tag \t append) +// recruiting (boolean) +// site +// date (epoch) +// picture +// active (boolean) +// info +// pcount +// player[x] = (name \t tag \t append \t guid) \t rank \t escape(title) \t online? +// hcount +// history[x] = (clan history entry) -> entry 0 is oldest +// icount +// invitee[x] = expiration \t (sender player) \t (recipient player) + +function tn_community_browser_wipeMemberships(%this) +{ + %count = %this.mcount; + for (%i = 0; %i < %count; %i++) + { + %this.membership[%i] = ""; + } + %this.mcount = ""; + + %count = %this.icount; + for (%i = 0; %i < %count; %i++) + { + %this.invitee[%i] = ""; + } + %this.icount = ""; +} + +function tn_community_browser_wipePlayers(%this) +{ + %count = %this.pcount; + for (%i = 0; %i < %count; %i++) + { + %this.player[%i] = ""; + } + %this.pcount = ""; +} + +function tn_community_browser_getPlayerProfile(%guid) +{ + if (isObject($Browser::PlayerProfileTable[%guid])) + return $Browser::PlayerProfileTable[%guid]; + + %profile = new SimObject() + { + classname = BrowserPlayer; + guid = %guid; + }; + $Browser::PlayerProfileTable[%guid] = %profile; + + $BrowserPlayerSet.add(%profile); + return %profile; +} + +function tn_community_browser_getClanProfile(%id) +{ + if (isObject($Browser::ClanProfileTable[%id])) + return $Browser::ClanProfileTable[%id]; + + %profile = new SimObject() + { + classname = BrowserClan; + id = %id; + }; + $Browser::ClanProfileTable[%id] = %profile; + + $BrowserClanSet.add(%profile); + return %profile; +} + +function tn_community_browser_initQueue() +{ + // initialize a message vector to handle queuing requests to the remote system + if (isObject($BrowserRequestQueue)) + $BrowserRequestQueue.delete(); + $BrowserRequestQueue = new MessageVector(); + + // initialize the browser player and clan object caches + if (isObject($BrowserPlayerSet)) + { + while ($BrowserPlayerSet.getCount() > 0) + $BrowserPlayerSet.getObject(0).delete(); + $BrowserPlayerSet.delete(); + } + if (isObject($BrowserClanSet)) + { + while ($BrowserClanSet.getCount() > 0) + $BrowserClanSet.getObject(0).delete(); + $BrowserClanSet.delete(); + } + $BrowserPlayerSet = new SimSet(); + $BrowserClanSet = new SimSet(); +} +tn_community_browser_initQueue(); + +function tn_community_browser_processRequest(%request, %payload) +{ + if (%request !$= "") + { + %request = "?guid=" @ getField($LoginCertificate, 1) @ "&uuid=" @ $TribesNext::Community::UUID @ "&" @ %request; + } + if (%payload $= "") + { + %data = "GET " @ $TribesNext::Community::BaseURL @ $TribesNext::Community::BrowserScript @ %request; + %data = %data @ " HTTP/1.1\r\nHost: " @ $TribesNext::Community::Host @ "\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + } + else + { + %data = "POST " @ $TribesNext::Community::BaseURL @ $TribesNext::Community::BrowserScript @ " HTTP/1.1\r\n"; + %data = %data @ "Host: " @ $TribesNext::Community::Host @ "\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n"; + %data = %data @ %payload; + } + + $BrowserRequestQueue.pushBackLine(%data); + + if (!$TribesNext::Community::Browser::Active) + tn_community_browser_executeNextRequest(); +} + +function tn_community_browser_executeNextRequest() +{ + if ($BrowserRequestQueue.getNumLines() <= 0) + return; + + %data = $BrowserRequestQueue.getLineText(0); + $BrowserRequestQueue.popFrontLine(); + + $TribesNext::Community::Browser::Active = 1; + + if (isObject(CommunityBrowserInterface)) + { + CommunityBrowserInterface.disconnect(); + } + else + { + new TCPObject(CommunityBrowserInterface); + } + CommunityBrowserInterface.data = %data; + CommunityBrowserInterface.connect($TribesNext::Community::Host @ ":" @ $TribesNext::Community::Port); +} + + +// URL escape a string +function tn_community_browser_urlescape(%string) +{ + // this function transforms all characters into %xx form, regardless of whether + // it is necessary to actually encode them + %out = ""; + %len = strlen(%string); + for (%i = 0; %i < %len; %i++) + { + %c = getSubStr(%string, %i, 1); + %hex = DecToHex(strCmp(%c, "")); + while (strlen(%hex) < 2) + %hex = "0" @ %hex; + %out = %out @ "%" @ %hex; + } + + return %out; +} + +// implementation of API requests + +function tn_community_browser_request_cert() +{ + error("Browser: Downloading enhanced certificate from community server."); + tn_community_browser_processRequest("method=cert"); +} + +// * * * clan data accessor API methods (these do not require authentication) + +// search for clans by name, returns a list of full-names and clan ID numbers +function tn_community_browser_clan_search(%name) +{ + $Browser::CCount = ""; + %name = tn_community_browser_urlescape(%name); + tn_community_browser_processRequest("method=csearch&a0=" @ %name); +} + +// views clan information for a given clan (e.g. memberships, info, website, recruiting, etc.) +function tn_community_browser_clan_view(%id) +{ + tn_community_browser_processRequest("method=cview&a0=" @ %id); +} + +// views clan history (e.g. renamings, retaggings, invitations, kickings, ranking) +function tn_community_browser_clan_history(%id) +{ + tn_community_browser_processRequest("method=chist&a0=" @ %id); +} + +// * * * clan management API methods (these require authentication, membership, and sufficient rank) + +// set the recruiting yes/no flag on the clan (alteration rank) +function tn_community_browser_clan_recruiting(%id, %set) +{ + tn_community_browser_processRequest("method=crecr&a0=" @ %id @ "&a1=" @ %set); +} + +// set the clan info field string (alteration rank) +function tn_community_browser_clan_info(%id, %info) +{ + %guid = getField($LoginCertificate, 1); + %uuid = $TribesNext::Community::UUID; + + %boundary = "-------------------------"; + %rand = getRandom(10000, 99999) @ getRandom(10000, 99999) @ getRandom(10, 9999); + %formelem = "Content-Disposition: form-data; name=\""; + + %payload = "--" @ %boundary @ %rand @ "\r\n"; + + // GUID element + %payload = %payload @ %formelem @ "guid\"\r\n\r\n" @ %guid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // UUID + %payload = %payload @ %formelem @ "uuid\"\r\n\r\n" @ %uuid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // method + %payload = %payload @ %formelem @ "method\"\r\n\r\ncinfo\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // id + %payload = %payload @ %formelem @ "a0\"\r\n\r\n" @ %id @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // info + %payload = %payload @ %formelem @ "a1\"\r\n\r\n" @ %info @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + %header = "Content-Type: multipart/form-data; boundary=" @ %boundary @ %rand @ "\r\n"; + %header = %header @ "Content-Length: " @ strlen(%payload) @ "\r\n\r\n"; + + tn_community_browser_processRequest("", %header @ %payload); +} + +// set the clan tag, and whether it is prepended or appended to player names (alteration rank) +function tn_community_browser_clan_retag(%id, %tag, %append) +{ + %tag = tn_community_browser_urlescape(%tag); + tn_community_browser_processRequest("method=ctag&a0=" @ %id @ "&a1=" @ %tag @ "&a2=" @ %append); +} + +// set the clan website string (alteration rank) +function tn_community_browser_clan_website(%id, %website) +{ + %website = tn_community_browser_urlescape(%website); + tn_community_browser_processRequest("method=csite&a0=" @ %id @ "&a1=" @ %website); +} + +// set the clan name string (alteration rank) +function tn_community_browser_clan_rename(%id, %name) +{ + %name = tn_community_browser_urlescape(%name); + tn_community_browser_processRequest("method=cname&a0=" @ %id @ "&a1=" @ %name); +} + +// set the filename of the displayed clan picture in the in-game browser (alteration rank) +function tn_community_browser_clan_picture(%id, %picture) +{ + %picture = tn_community_browser_urlescape(%picture); + tn_community_browser_processRequest("method=cpict&a0=" @ %id @ "&a1=" @ %picture); +} + +// invite another player (by GUID) to this clan (recruitment rank) +function tn_community_browser_clan_sendInvite(%id, %invitee) +{ + tn_community_browser_processRequest("method=cinvite&a0=" @ %id @ "&a1=" @ %invitee); +} + +// cancel another player's (by GUID) invitation to this clan (recruitment rank, for self invites -- admin for any invite) +function tn_community_browser_clan_retractInvite(%id, %invitee) +{ + tn_community_browser_processRequest("method=cretract&a0=" @ %id @ "&a1=" @ %invitee); +} + +// set a list of outstanding invitations for a clan (recruitment rank) +function tn_community_browser_clan_viewInvites(%id) +{ + tn_community_browser_processRequest("method=cinvview&a0=" @ %id); +} + +// change the rank/title of a user in the clan +// if invoked on self, this works so long as the member is not of probation rank +// if invoked on others, this requires alteration rank -- it can raise others to the user's rank +// but, it cannot be used to reduce the rank of someone at the same (or greater) level +function tn_community_browser_clan_changeRank(%id, %target, %rank, %title) +{ + %title = tn_community_browser_urlescape(%title); + tn_community_browser_processRequest("method=crank&a0=" @ %id @ "&a1=" @ %target @ "&a2=" @ %rank @ "&a3=" @ %title); +} + +// sets authorization status for a clan disband +// must be administrative rank, and must be consensus of at least 50% to complete the disband +// authorization can be de-set if an administrative rank user changes their mind +// if 50% of administrative rank users authorize disband, the disband will happen instantly +// when serving this request +function tn_community_browser_clan_disband(%id, %set) +{ + tn_community_browser_processRequest("method=cdisb&a0=" @ %id @ "&a1=" @ %set); +} + + +// kick a user from the clan +// requires alteration rank, but cannot kick users at same (or greater) level +function tn_community_browser_clan_kick(%id, %target) +{ + tn_community_browser_processRequest("method=ckick&a0=" @ %id @ "&a1=" @ %target); +} + +// * * * user data accessor API methods (do not require authentication) + +// search for players by name -- returns full matching names and GUIDs +function tn_community_browser_user_search(%name) +{ + %name = tn_community_browser_urlescape(%name); + $Browser::PCount = ""; + tn_community_browser_processRequest("method=usearch&a0=" @ %name); +} + +// view info for a user -- profile, clan memberships, registration date, etc. +function tn_community_browser_user_view(%guid) +{ + tn_community_browser_processRequest("method=uview&a0=" @ %guid); +} + +// view history for a user -- clan creations/joins/leavings/kicks/kicked/disband/renamings +function tn_community_browser_user_history(%guid) +{ + tn_community_browser_processRequest("method=uhist&a0=" @ %guid); +} + +// * * * user profile management API methods (require authentication as the player him/herself) + +// request a name-change, this will be fulfilled it if is unused for raw accounts, unused +// in a prior rename browser-side, and deemed acceptable to the existing naming policy +function tn_community_browser_user_rename(%name) +{ + %name = tn_community_browser_urlescape(%name); + tn_community_browser_processRequest("method=uname&a0=" @ %name); +} + +// sets the active displayed clan to the given ID. By setting -1, it is possible to retain +// clan memberships, but not show a tag (this was not possible in the Dynamix browser) +function tn_community_browser_user_activeClan(%id) +{ + tn_community_browser_processRequest("method=uclan&a0=" @ %id); +} + +// set profile website link +function tn_community_browser_user_website(%website) +{ + %website = tn_community_browser_urlescape(%website); + tn_community_browser_processRequest("method=usite&a0=" @ %website); +} + +// set the filename of the displayed user picture in the in-game browser +function tn_community_browser_user_picture(%picture) +{ + %picture = tn_community_browser_urlescape(%picture); + tn_community_browser_processRequest("method=upict&a0=" @ %picture); +} + +// set info string to given data +function tn_community_browser_user_info(%info) +{ + %guid = getField($LoginCertificate, 1); + %uuid = $TribesNext::Community::UUID; + + %boundary = "-------------------------"; + %rand = getRandom(10000, 99999) @ getRandom(10000, 99999) @ getRandom(10, 9999); + %formelem = "Content-Disposition: form-data; name=\""; + + %payload = "--" @ %boundary @ %rand @ "\r\n"; + + // GUID element + %payload = %payload @ %formelem @ "guid\"\r\n\r\n" @ %guid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // UUID + %payload = %payload @ %formelem @ "uuid\"\r\n\r\n" @ %uuid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // method + %payload = %payload @ %formelem @ "method\"\r\n\r\nuinfo\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // info + %payload = %payload @ %formelem @ "a0\"\r\n\r\n" @ %info @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + %header = "Content-Type: multipart/form-data; boundary=" @ %boundary @ %rand @ "\r\n"; + %header = %header @ "Content-Length: " @ strlen(%payload) @ "\r\n\r\n"; + + tn_community_browser_processRequest("", %header @ %payload); +} + +// accept an invitation to the specified clan ID -- errors if no such invite exists or is invalid +// can also produce an error if the user is in the maximum number of clans (but will not burn the +// invite in the process -- can retry after leaving an existing clan) +function tn_community_browser_user_acceptInvite(%id) +{ + tn_community_browser_processRequest("method=uaccept&a0=" @ %id); +} + +// ditto to above, but rejects the invite instead +function tn_community_browser_user_rejectInvite(%id) +{ + tn_community_browser_processRequest("method=ureject&a0=" @ %id); +} + +// leave a clan -- this will always succeed provided the user was a member of it. +// if the user was actively using this clan's tag, and they are still a member of at least one +// clan, the active tag will be set to the clan that the user has been a member of the longest +function tn_community_browser_user_leaveClan(%id) +{ + tn_community_browser_processRequest("method=uleave&a0=" @ %id); +} + +// create a clan -- specify the tag/name/info +function tn_community_browser_user_createClan(%tag, %append, %name, %info, %recruiting) +{ + // do as POST since the contents can be longer than URI length limits + %guid = getField($LoginCertificate, 1); + %uuid = $TribesNext::Community::UUID; + + %boundary = "-------------------------"; + %rand = getRandom(10000, 99999) @ getRandom(10000, 99999) @ getRandom(10, 9999); + %formelem = "Content-Disposition: form-data; name=\""; + + %payload = "--" @ %boundary @ %rand @ "\r\n"; + + // GUID element + %payload = %payload @ %formelem @ "guid\"\r\n\r\n" @ %guid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // UUID + %payload = %payload @ %formelem @ "uuid\"\r\n\r\n" @ %uuid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // method + %payload = %payload @ %formelem @ "method\"\r\n\r\ncreate\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // tag + %payload = %payload @ %formelem @ "a0\"\r\n\r\n" @ %tag @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // append + %payload = %payload @ %formelem @ "a1\"\r\n\r\n" @ %append @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // name + %payload = %payload @ %formelem @ "a2\"\r\n\r\n" @ %name @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // info + %payload = %payload @ %formelem @ "a3\"\r\n\r\n" @ %info @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // append + %payload = %payload @ %formelem @ "a4\"\r\n\r\n" @ %recruiting @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + %header = "Content-Type: multipart/form-data; boundary=" @ %boundary @ %rand @ "\r\n"; + %header = %header @ "Content-Length: " @ strlen(%payload) @ "\r\n\r\n"; + + tn_community_browser_processRequest("", %header @ %payload); +} + +schedule(3000, 0, tn_community_Browser_request_cert); \ No newline at end of file diff --git a/t2csri/community/browserUI.cs b/t2csri/community/browserUI.cs new file mode 100644 index 0000000..16e6137 --- /dev/null +++ b/t2csri/community/browserUI.cs @@ -0,0 +1,1943 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2012-2013 + +// Tribes 2 Community System +// Browser UI Coercion + +// This script implements connectivity between the Dynamix browser UI shipped with Tribes 2 and the community +// systems developed for TribesNext. The comunication to the TribesNext system via network is implemented in +// the robot client data interface script for the browser. This script merely connects (modified) Dynamix UI +// elements to query/invoke methods on this new data interface, instead of the IRC server command used initially. + +// Several functional changes were made as part of this process. First, all players are now keyed by GUID, and +// all clans are now keyed by ClanID (CID). Neither clan or player names are immutable, and it is foolish to +// treat them as such. Secondly, in the initial Dynamix system, clan names were set during creation and could +// not be changed without disbanding and recreating a clan; clan names can now be changed in the new system. +// Thirdly, disbanding a clan now requires at least 50% consensus among rank 4 administrators to proceed with +// the clan disband. Fourth, all history elements for both the player and clan pages now track all major +// profile modification events, and include live links to the affected/affector player/clan profiles. These +// live links automatically reflect changes to the name and active tag of those entities. Fifth, clans are +// never fully deleted. If all members of a clan leave, or a disband consensus is reached, the clan is set +// to "inactive" status. The name and tag become available again for use by others, and it will not appear +// in searches for the clan name, but the "tombstone" of the clan can still be accessed by clicking on one +// of the "live" links in the history of any player who was a member. The tombstone will include full history, +// including who was responsible for initiating the dissolution actions. This sort of auditing should make +// players slightly more accountable, and disincentivize sabotaging a clan (since it cannot be done anonymously +// as it could in the original system). Finally, to prevent a clan from becoming leaderless, and breaking the +// ability to administer it (e.g. all rank 4 administrators leave), the browser system guarantees that any +// active clan (e.g. with at least one player) has a rank 4 by promoting an existing member. One user will be +// promoted automatically in a leaderless clan situation, and this user is decided by a combination of rank +// with join date as tie-breaker at the top rank level. + +// redownload a clan profile only if at least this amount of time has passed since the last download +$TribesNext::Community::BrowserUI::MinRefreshTime = 30000; + +// modify a few user interface elements to match some of the data rearrangements +function tn_community_browserui_modifyUIElements() +{ + // change the second button from "roster" to "history" + // the clan roster will now always be visible in the player list portion + // of the user interface -- invites are now displayed in the center + // pane to allow more room to manage the additional invite information/options + TL_Roster.setText("HISTORY"); + TL_Roster.setExtent(67, 27); + + // nudge the options/invites/admin buttons 2 pixels to the right + // but defined relative to TL_Roster's position + // note: TL_News is labeled as "OPTIONS" in the UI -- blame Dynamix + %pos = VectorAdd(TL_Roster.getPosition(), "64 0"); + TL_News.setPosition(getWord(%pos, 0), getWord(%pos, 1)); + %pos = VectorAdd(TL_News.getPosition(), "66 0"); + TL_Invites.setPosition(getWord(%pos, 0), getWord(%pos, 1)); + %pos = VectorAdd(TL_Invites.getPosition(), "63 0"); + TW_Admin.setPosition(getWord(%pos, 0), getWord(%pos, 1)); + + // set all of the buttons to the same group number + TW_Admin.groupNum = 4; + TL_Invites.groupNum = 4; + TL_Profile.groupNum = 4; + TL_Roster.groupNum = 4; + TL_News.groupNum = 4; + + // in the tribe admin panel: edit the tag max length to 12 -- looks like Dynamix had this at 9 + TP_NewTag.maxLength = 12; + TP_TribeTagBtn.text = "CHANGE TAG"; + // add a field to allow supporting changes to the clan name + %nameField = new ShellTextEditCtrl(); + %nameField.maxLength = 40; + %nameField.setPosition(0, 243); + %nameField.setExtent(335, 64); + ProfileControl.add(%nameField); + TribePropertiesDlg.nameField = %nameField; + // ... and a button to apply it + %renameBtn = new ShellBitmapButton(); + %renameBtn.text = "RENAME"; + %renameBtn.extent = "70 38"; + %renameBtn.position = "320 243"; + %renameBtn.command = "TribePropertiesDlg.RenameTribe();"; + ProfileControl.add(%renameBtn); + + // in the member profile editor for clans + tb_onProbation.setText("RANK 0: Probationary Member "); + tb_tribeMember.setText("RANK 1: Standard Member "); + tb_tribeAdmin.setText("RANK 2: Invitation Issuer "); + tb_tribeController.setText("RANK 3: Secondary Administrator"); + tb_sysAdmin.setText("RANK 4: Primary Administrator "); +} +tn_community_browserui_modifyUIElements(); + +// ========================================================================= +// User interface update hook to data interface. +// ========================================================================= +function tn_community_browserui_clearCheckStatus() +{ + if (isEventPending($TribesNext::Community::BrowserUI::StatusSchedule)) + cancel($TribesNext::Community::BrowserUI::StatusSchedule); + + if ($TribesNext::Community::Browser::Active) + { + $TribesNext::Community::BrowserUI::StatusSchedule = schedule(32, 0, tn_community_browserui_clearCheckStatus); + return; + } + error("Browser UI update hook occured."); + + if (TribeAndWarriorBrowserGui.searchActive) + { + TribeAndWarriorBrowserGui.searchActive = 0; + tn_community_browserui_displaySearchResults(); + } + if (TribePane.updateActive) + { + TribePane.updateActive = 0; + tn_community_browserui_showTribePane(); + } + if (PlayerPane.updateActive) + { + PlayerPane.updateActive = 0; + tn_community_browserui_showPlayerPane(); + } + + // reset the cursor to non-wait mode + Canvas.setCursor(defaultCursor); +} + +// ========================================================================= +// "WARRIOR SEARCH" and "TRIBE SEARCH" +// ========================================================================= + +// replacing function in webbrowser.cs, 618 +function SearchWarriors() +{ + if(BrowserSearchPane.query !$= "player") + { + // clear out the fields... + $BrowserSearchField = ""; + BrowserSearchMatchList.clear(); + } + Canvas.pushDialog(BrowserSearchDlg); + BrowserSearchPane.setTitle("WARRIOR SEARCH"); + BrowserSearchPane.query = "player"; + Search_EditField.makeFirstResponder(1); +} + +// replacing function in webbrowser.cs, 321 +function SearchTribes() +{ + if(BrowserSearchPane.query !$= "clan") + { + // clear out the fields... + $BrowserSearchField = ""; + BrowserSearchMatchList.clear(); + } + + Canvas.pushDialog(BrowserSearchDlg); + Search_EditField.makeFirstResponder(1); + BrowserSearchPane.setTitle("TRIBE SEARCH"); + BrowserSearchPane.query = "clan"; +} + +// replacing function in webbrowser.cs, 57 +function BrowserStartSearch() +{ + // server will reject blank searches, save a round trip by also checking here + %search = trim(strreplace($BrowserSearchField, "%", "")); + if(%search $="") + { + MessageBoxOK("NOTICE","Blank searches are not allowed; enter one or more characters of text and try again.","Search_EditField.makeFirstResponder(1);"); + } + else + { + // removed the Dynamix text validation code from here; relying on client side + // validation makes for fragile systems + BrowserSearchPane.key = LaunchGui.key++; + + BrowserSearchMatchList.clear(); + canvas.SetCursor(ArrowWaitCursor); + if(isEventPending(TribeAndWarriorBrowserGui.eid)) + cancel(TribeAndWarriorBrowserGui.eid); + + if(BrowserSearchPane.query $= "player") + { + BrowserSearchPane.state = "warriorSearch"; + tn_community_browser_user_search($BrowserSearchField); + } + else + { + BrowserSearchPane.state = "tribeSearch"; + tn_community_browser_clan_search($BrowserSearchField); + } + + //TribeAndWarriorBrowserGui.eid = schedule(250,0,ExecuteSearch,0,BrowserSearchPane); + TribeAndWarriorBrowserGui.searchActive = 1; + tn_community_browserui_clearCheckStatus(); + } +} + +// replacing function in webbrowser.cs, 16 +function BrowserSearchDone() +{ + Canvas.popDialog(BrowserSearchDlg); + %id = BrowserSearchMatchList.getSelectedId(); + if(%id != -1) + { + %row = BrowserSearchMatchList.getRowTextById(%id); + echo(%id SPC %row); + if(BrowserSearchPane.query $= "clan") + TWBTabView.view(%id, %row, "Tribe"); + else + TWBTabView.view(%id, %row); + } +} + +function tn_community_browserui_displaySearchResults() +{ + if (BrowserSearchPane.query $= "clan") + { + %count = $Browser::CCount; + if (%count $= "") // no results + return; + for (%i = 0; %i <= %count; %i++) + { + %clan = $Browser::CResults[%i]; + %cid = getField(%clan, 0); + %cname = getField(%clan, 1); + + BrowserSearchMatchList.addRow(%cid, %cname); + } + } + else + { + %count = $Browser::PCount; + if (%count $= "") // no results + return; + for (%i = 0; %i <= %count; %i++) + { + %player = $Browser::PResults[%i]; + %pname = getField(%player, 0); + //%ptag = getField(%player, 1); + //%pappend = getField(%player, 2); + %pguid = getField(%player, 3); + + BrowserSearchMatchList.addRow(%pguid, %pname); + } + } +} + +// ========================================================================= +// Profile Viewing Utilities +// ========================================================================= + +// replacing function in webbrowser.cs, 683 +function TribeAndWarriorBrowserGui::onWake(%this) +{ + MemberList.ClearColumns(); + W_MemberList.ClearColumns(); + MemberList.Clear(); + W_MemberList.clear(); + Canvas.pushDialog(LaunchToolbarDlg); + + if (TWBTabView.tabCount() == 0) + { + %info = WONGetAuthInfo(); + + // decode the enhanced certificate + %cert = $T2CSRI::CommunityCertificate; + if (getFieldCount(%cert) >= 5) + %authInfo = getField(%cert, 4); + + %len = strlen(%authInfo); + if (%len == 0) + return ""; + for (%i = 0; %i < %len; %i += 2) + { + %byte = getSubStr(%authInfo, %i, 2); + %char = collapseEscape("\\x" @ %byte); + %raw = %raw @ %char; + } + + // Open the player's page: + %myguid = getField(WONGetAuthInfo(), 3); + // (get current name from the enhanced cert) + TWBTabView.view(%myguid, getField(%raw, 0)); + w_profile.setValue(1); + + // Add tabs for the player's tribal pages: + %rcount = getRecordCount(%raw); + for (%i = 2; %i < %rcount; %i++) + { + %record = getRecord(%raw, %i); + %cid = getField(%record, 3); + %name = getField(%record, 0); + TWBTabView.view(%cid, %name, "Tribe"); + } + + // select the player's profile after loading clan tabs + TWBTabView.setSelected(%myguid); + + } + else if(PlayerPane.visible) + PlayerPane.onWake(); + else + TribePane.onWake(); +} + +// replacing function in webbrowser.cs, 995 +function TWBTabView::onSelect(%this, %id, %text) +{ + %tabSet = %this.getTabSet(%id); + %myguid = getField(WONGetAuthInfo(), 3); + + MemberList.clear(); + W_MemberList.clear(); + TWBScroll.scrollToTop(); + TWBTitle.OldText = TWBTitle.name; + TWBTitle.setValue(%text); // This will get overwritten... + TWBTitle.name = %text; + TWBClosePaneBtn.setVisible(true); + switch(%tabSet) + { + case 0: // Warrior + if(isObject(TProfileHdr)) + { + TProfileHdr.delete(); + new GuiControl(TProfileHdr); + } + PlayerPane.setvisible(1); + TribePane.setvisible(0); + + if(W_memberList.rowCount()<=0) + PlayerPane.needRefresh = 1; + else + PlayerPane.needRefresh = 0; + + TWBTabFrame.setAltColor(false); + + %isMe = (%id == %myguid); + + TWBClosePaneBtn.setVisible(!%isMe); + if(TWBTitle.OldText !$= TWBTitle.name) + W_Profile.setValue(1); + + PlayerPix.setBitmap($playerGfx); + W_Profile.setVisible(1); + W_History.setVisible(1); + W_Tribes.setVisible(1); + + if(%isMe) + { + W_BuddyList.setText("BUDDYLIST"); + W_BuddyList.setVisible(1); + W_BuddyList.command = "PlayerPane.ButtonClick(3);"; + W_BuddyList.groupNum = 5; + } + else + { + W_BuddyList.setText("OPTIONS"); + W_BuddyList.setVisible(1); + W_BuddyList.command = "PlayerPane.ButtonClick(4);"; + W_BuddyList.groupNum = 4; + } + W_Admin.setVisible(%isMe); + + case 1: // Tribe + PlayerPane.setvisible(0); + TribePane.setvisible(1); + if(memberList.rowCount()<=0) + TribePane.needRefresh = 1; + else + TribePane.needRefresh = 0; + + TWBTabFrame.setAltColor(true); + if(TWBTitle.OldText !$= TWBTitle.name) + TL_Profile.setValue(1); + + //%this.display(); + + } +} + +// replacing function in webbrowser.cs, 1063 +function GuiMLTextCtrl::onURL(%this, %url) +{ + %i = 0; + while((%fld[%i] = getField(%url, %i)) !$= "") + %i++; + + %tribe = %fld[1]; + %warrior = %fld[2]; + switch$(%fld[0]) + { + case "player": + LinkBrowser( %fld[1] , "Warrior"); + case "clan": // used to be "tribe" in the Dynamix system -- it is this in TribesNext + LaunchTabView.viewTab("BROWSER", TribeAndWarriorBrowserGui, 0); + TWBTabView.view(%fld[1], "", "Tribe"); + + case "wwwlink": + LinkWeb( %fld[1] ); + + case "retract": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + %target = %fld[2]; + %player = tn_community_browser_getPlayerProfile(%target); + %tname = %player.name; + if (%tname $= "") + %tname = %fld[3]; + + MessageBoxYesNo("RETRACT", "Are you sure you wish to retract\n" @ %tname @ "'s invite to\n\"" @ %clan.name @ "\"?", + "tn_community_browser_clan_retractInvite(" @ expandEscape(%fld[1]) @ ", " @ expandEscape(%target) @ ");", ""); + + case "acceptinvite": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + %cname = %clan.name; + if (%cname $= "") + %cname = %fld[2]; + + MessageBoxYesNo("INVITATION", "Accept invitation to join\n\"" @ %cname @ "\"?", + "tn_community_browser_user_acceptInvite(" @ expandEscape(%fld[1]) @ ");", ""); + + case "rejectinvite": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + %cname = %clan.name; + if (%cname $= "") + %cname = %fld[2]; + + MessageBoxYesNo("INVITATION", "Are you sure you want to REJECT invitation to join\n\"" @ %cname @ "\"?", + "tn_community_browser_user_rejectInvite(" @ expandEscape(%fld[1]) @ ");", ""); + + case "email": // TribesNext version + LinkEMail(getFields(%url, 1)); + + case "invite": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + %target = %fld[2]; + %player = tn_community_browser_getPlayerProfile(%target); + %tname = %player.name; + if (%tname $= "") + %tname = %fld[3]; + + MessageBoxYesNo("RETRACT", "Are you sure you wish to invite\n" @ %tname @ " to join\n\"" @ %clan.name @ "\"?", + "tn_community_browser_clan_sendInvite(" @ expandEscape(%fld[1]) @ ", " @ expandEscape(%target) @ ");", ""); + + case "addBuddy": // TribesNext Version + MessageBoxYesNo("CONFIRM","Add \"" @ %fld[2] @ "\" to Buddy List?", + "tn_community_mail_request_addListEntry(\"buddy\", \"" @ expandEscape(%fld[1]) @ "\"); PlayerPane.updateActive = 1; schedule(300, 0, tn_community_browserui_clearCheckStatus);",""); + case "delBuddy": // TribesNext Version + MessageBoxYesNo("CONFIRM","Remove \"" @ %fld[2] @ "\" from Buddy List?", + "tn_community_mail_request_delListEntry(\"buddy\", \"" @ expandEscape(%fld[1]) @ "\"); PlayerPane.updateActive = 1; schedule(300, 0, tn_community_browserui_clearCheckStatus);",""); + + case "gamelink": // Leave this alone -- the score HUD uses this for interactivity with game servers + commandToServer('ProcessGameLink', %fld[1], %fld[2], %fld[3], %fld[4], %fld[5]); + + case "joinPublicChat": // FUTURE Implement for TribesNext? + joinPublicTribeChannel(getField(%url,1)); + case "joinPrivateChat": // FUTURE Implement for TribesNext? + joinPrivateTribeChannel(getField(%url,1)); + + case "activeclan": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + MessageBoxYesNo("CONFIRM", "Are you sure you wish to set \n\"" @ %clan.name @ "\"\n as your active clan?", + "tn_community_browser_user_activeClan(" @ expandEscape(%fld[1]) @ "); PlayerPane.updateActive = 1; tn_community_browserui_clearCheckStatus();",""); + + case "leaveclan": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + MessageBoxYesNo("CONFIRM", "Are you sure you wish to leave \n\"" @ %clan.name @ "\"?", + "tn_community_browser_user_leaveClan(" @ expandEscape(%fld[1]) @ "); PlayerPane.updateActive = 1; tn_community_browserui_clearCheckStatus();",""); + + case "emailclan": // TribesNext version + %clan = tn_community_browser_getClanProfile(%fld[1]); + %pcount = %clan.pcount; + %records = ""; + if (%pcount !$= "") + { + for (%i = 0; %i <= %pcount; %i++) + { + %player = %clan.player[%i]; + %memberguid = getField(%player, 3); + %membername = getField(%player, 0); + %membertag = getField(%player, 1); + %memberappend = getField(%player, 2); + %record = %memberguid TAB %membername TAB %membertag TAB %memberappend; + if (%memberguid != getField(WonGetAuthInfo(), 3)) + %records = %records @ "\n" @ %record; + } + %records = trim(%records); + } + LinkEMail(%records); + + //if there is an unknown URL type, treat it as a weblink.. + default: + LinkWeb( %fld[0] ); + } +} + +// ========================================================================= +// Tribe Profile Viewing +// ========================================================================= + +// replacing function in webbrowser.cs, 960 +function TWBTabView::view(%this, %id, %name, %type) +{ + if ( %type $= "Tribe" ) + %tabSet = 1; + else + %tabSet = 0; + + // see if we already have a tab with this clanid + if (%this.getTabIndex(%id) != -1) + { + %this.setSelected(%id); + return; + } + + // Or else add the new tab: + %this.addTab(%id, %name, %tabSet); + %this.setSelected(%id); + + if (%tabSet == 1) + { + TribePane.targetid = %id; + tn_community_browserui_showTribePane(); + } + else + { + PlayerPane.targetid = %id; + tn_community_browserui_showPlayerPane(); + } +} + +// replacing function in webbrowser.cs, 1550 +function TribePane::ButtonClick(%this, %senderid) +{ + canvas.SetCursor(ArrowWaitCursor); + + %tribeName = TWBTabView.getSelectedText(); + %clanid = TWBTabView.getSelectedId(); + + %this.tabstate = "TRIBE"; + %this.targetid = %clanid; + %this.state = "NONE"; + + %now = getSimTime(); + if (%now < $TribesNext::Community::BrowserUI::MinRefreshTime) + %now = ($TribesNext::Community::BrowserUI::MinRefreshTime + 1); + + switch(%senderid) + { + case 0: //PROFILE + %this.state = "VIEW_CLAN"; + %clanobj = tn_community_browser_getClanProfile(%clanid); + %delta = %now - %clanobj.lastRefresh; + + if ($TribesNext::Community::BrowserUI::MinRefreshTime < %delta) + { + %this.updateActive = 1; + tn_community_browser_clan_view(%clanid); + tn_community_browserui_clearCheckStatus(); + } + else + { + tn_community_browserui_showTribePane(); + } + + case 1: //Formerly "Roster", now "History" + %this.state = "CLAN_HISTORY"; + %clanobj = tn_community_browser_getClanProfile(%clanid); + %delta = %now - %clanobj.lastHistRefresh; + + if ($TribesNext::Community::BrowserUI::MinRefreshTime < %delta) + { + %this.updateActive = 1; + %clanobj.lastHistRefresh = %now; + tn_community_browser_clan_history(%clanid); + tn_community_browserui_clearCheckStatus(); + } + else + { + tn_community_browserui_showTribePane(); + } + + case 2: // "OPTIONS" button -- for some reason this was called "News" in the Dynamix code + %this.state = "CLAN_OPTIONS"; + tn_community_browserui_showTribePane(); + + case 3: //INVITE BUTTON + %this.state = "CLAN_INVITES"; + TribePane.updateActive = 1; + tn_community_browser_clan_viewInvites(%clanid); + tn_community_browserui_clearCheckStatus(); + case 4: //Admin Tribe + TribePropertiesDlg.pendingChanges = ""; + Canvas.PushDialog(TribePropertiesDlg); + } +} + +// replacing function in webbrowser.cs, 1545 +function TribePane::RosterDblClick(%this) +{ + LaunchBrowser(MemberList.getSelectedId(), "Warrior"); +} + +function tn_community_browserui_amIMember(%clanid) +{ + return (tn_community_browserui_myRankIn(%clanid) >= 0); +} + +function tn_community_browserui_myRankIn(%clanid) +{ + // this pulls out data from the community certificate sent by the browser system + %cert = $T2CSRI::CommunityCertificate; + if (getFieldCount(%cert) >= 5) + %authInfo = getField(%cert, 4); + + // decode the hex version of the auth info + %len = strlen(%authInfo); + if (%len == 0) + return -1; + for (%i = 0; %i < %len; %i += 2) + { + %byte = getSubStr(%authInfo, %i, 2); + %char = collapseEscape("\\x" @ %byte); + %raw = %raw @ %char; + } + + %rcount = getRecordCount(%raw); + for (%i = 2; %i < %rcount; %i++) + { + %record = getRecord(%raw, %i); + %cid = getField(%record, 3); + %rank = getField(%record, 4); + + if (%cid == %clanid) + return %rank; + } + + return -1; +} + +function tn_community_browserui_showTribePane() +{ + %this = TribePane; + %clanid = %this.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + + %name = %clan.name; + %tag = getField(%clan.tag, 0); + %recru = %clan.recruiting; + %site = %clan.site; + %date = %clan.date; + %pict = %clan.picture; + %active = %clan.active; + %info = %clan.info; + + %isMember = tn_community_browserui_amIMember(%clanid); + %myRank = tn_community_browserui_myRankIn(%clanid); + + // hide inaccessible buttons for certain ranks + if (%myRank < 3) + { + // under administrative rank + TW_Admin.setVisible(false); + } + else + { + TW_Admin.setVisible(true); + } + + if (%myRank < 2) + { + // under invitation rank + TL_Invites.setVisible(false); + } + else + { + TL_Invites.setVisible(true); + } + + switch$ (%this.state) + { + case "VIEW_CLAN": + TWBText.clear(); + if (%name !$= "") + { + TWBTabView.setTabText(%clanid, %name); + } + + // active status + if (!%active && %name !$= "") + { + %activeMsg = "\"" @ strupr(%name) @ "\" IS INACTIVE -- THIS IS A SNAPSHOT TAKEN AT TIME OF DEACTIVATION\n"; + } + if (%name $= "") + { + %activeMsg = "Loading...\n"; + } + + // variant of what the Dynamix system originally showed at the top of clan pages + %Tdesc = ""; + %Tdesc = %Tdesc @ "Created: " @ %date @ "\n"; + %Tdesc = %Tdesc @ "Website: " @ %site @ "\n"; + %Tdesc = %Tdesc @ "Recruiting: "; + %Tdesc = %Tdesc @ ((%recru && %active) ? (%isMember ? "YES" : "YES Request Invite") : "NO"); + %Tdesc = %Tdesc @ "" NL ""; + + // set the window title + TWBTitle.name = %name; + TWBTitle.setValue(%name TAB %tag); + + // show the clan picture (or default if none) + if (%pict !$= "") + { + TeamPix.setBitmap(%pict); + } + else + TeamPix.setBitmap("texticons/twb/twb_Lineup.jpg"); + + // set the page description + TWBText.setText(%activeMsg @ %Tdesc NL %info); + + // populate the membership roster + MemberList.Clear(); + MemberList.ClearColumns(); + MemberList.clearList(); + + MemberList.addColumn( 0, "MEMBER", 92, 0, 100,"left"); + MemberList.addColumn( 1, "TITLE", 90, 0, 100,"left"); + MemberList.addColumn( 2, "RNK", 30, 0, 40, "numeric center"); + + %pcount = %clan.pcount; + if (%pcount !$= "") + { + for (%i = 0; %i <= %pcount; %i++) + { + %member = %clan.player[%i]; + + %mname = getField(%member, 0); + %mguid = getField(%member, 3); + %mrank = getField(%member, 4); + %mtitle = collapseEscape(getField(%member, 5)); + %monline = getField(%member, 6); + + MemberList.addRow(%mguid, %mname TAB %mtitle TAB %mrank TAB %clanid); + MemberList.setRowStylebyID(%mguid, !%monline); + } + } + // } end case VIEW_CLAN + case "CLAN_HISTORY": + TWBText.clear(); + %header = "History:\n\n"; + + %text = ""; + %hcount = %clan.hcount; + if (%hcount !$= "") + { + for (%i = %hcount; %i >= 0; %i--) + { + if (%clan.historyCache[%i] $= "") + { + %event = %clan.history[%i]; + %etype = getField(%event, 0); + %etime = tn_community_mailui_epochToDate(getField(%event, 1)); + %payload = collapseEscape(getField(%event, 2)); + %template = getField(%event, 3); + %player1 = getFields(%event, 4, 7); + %player2 = getFields(%event, 8); + %expanded = tn_community_browserui_expandTemplate(%template, %payload, %player1, %player2); + %line = "" @ %etime @ " " @ %expanded; + + %clan.historyCache[%i] = %line; + } + else + { + %line = %clan.historyCache[%i]; + } + + %text = %text @ %line @ "\n"; + } + } + + TWBText.setText(%header @ %text); + // } end case CLAN_HISTORY + case "CLAN_OPTIONS": + TWBText.clear(); + // this used to contain links to the clan forum, public IRC, and private IRC channels + // -- these are not really all that useful, so, instead... + // create link based options for a handful of TribesNext browser API calls + + // go to clan website + // email all members of the clan + // Set Active Clan + // Leave Clan + + // check membership in clan before displaying all options + %text = "" @ %name @ " Options:\n\n" @ + "Visit Website\n\n"; + if (%isMember) + { + %text = %text @ "E-mail Members\n" @ + "Set as Active Clan\n\n\n" @ + "Leave Clan"; + } + TWBText.SetText(%text); + // } end case CLAN_OPTIONS + case "CLAN_INVITES": + // since we need a few more options, this is drawn as a table in the main text area + // not the prettiest configuration in the world, but it will do for now + TWBText.clear(); + %header = "Pending Invitations:\n\n"; + + %text = ""; + %icount = %clan.icount; + if (%icount !$= "") + { + %sLenM = 0; + %rLenM = 0; + for (%i = 0; %i <= %icount; %i++) + { + // extract the data and determine longest values for padding calculation + %invite = %clan.invitee[%i]; + %expiration = tn_community_mailui_epochToDate(getField(%invite, 0)); + %sender = getFields(%invite, 1, 4); + %sLen = strlen(getField(%sender, 0)) + strlen(getField(%sender, 1)); + if (%sLen > %sLenM) + %sLenM = %sLen; + %recipient = getFields(%invite, 5, 8); + %rLen = strlen(getField(%recipient, 0)) + strlen(getField(%recipient, 1)); + if (%rLen > %rLenM) + %rLenM = %rLen; + + %line[%i, 0] = %expiration; + %line[%i, 1] = %sender; + %line[%i, 2] = %recipient; + } + + // produce header and footer lines + %separator = "+---------------------+"; + %indicator = "| Valid to Date/Time |"; + %indSend = "Sender"; + for (%i = 0; %i < (%sLenM + 2); %i++) + { + %separator = %separator @ "-"; + if (strlen(%indSend) <= %sLenM) + %indSend = " " @ %indSend; + } + %separator = %separator @ "+"; + %indRec = "Recipient"; + for (%i = 0; %i < (%rLenM + 2); %i++) + { + %separator = %separator @ "-"; + if (strlen(%indRec) < %rLenM) + %indRec = " " @ %indRec; + } + %separator = %separator @ "+---------+"; + + %text = %text @ %separator @ "\n"; + %indicator = %indicator @ "" @ %indSend @ " | " @ %indRec @ " | Retract |"; + %text = %text @ %indicator @ "\n"; + %text = %text @ %separator @ "\n"; + + for (%i = 0; %i <= %icount; %i++) + { + // draw padded versions of the data in a table form + %expiration = "" @ %line[%i, 0] @ ""; + %sender = %line[%i, 1]; + %recipient = %line[%i, 2]; + + %sPad = ""; + %rPad = ""; + %sLen = strlen(getField(%sender, 0)) + strlen(getField(%sender, 1)); + %rLen = strlen(getField(%recipient, 0)) + strlen(getField(%recipient, 1)); + + while((strlen(%sPad) + %sLen) < %sLenM) + %sPad = " " @ %sPad; + while((strlen(%rPad) + %rLen) < %rLenM) + %rPad = " " @ %rPad; + + %senderguid = getField(%sender, 3); + %sender = %sPad @ tn_community_browserui_liveLinkPlayer(%sender); + %recipientguid = getField(%recipient, 3); + %recipient = %rPad @ tn_community_browserui_liveLinkPlayer(%recipient); + + // rank 2 users have the ability to retract invites that they send, and rank 3+'s have the ability to retract any invite to the clan + if (%senderguid == getField(WONGetAuthInfo(), 3) || %myRank >= 3) + { + %retract = "Retract |"; + } + else + { + %retract = " |"; + } + + %tLine = "| " @ %expiration @ " | " @ %sender @ " | " @ %recipient @ " | " @ %retract; + %text = %text @ %tLine @ "\n"; + } + %text = %text @ %separator @ "\n"; + } + else + { + %text = "There are no pending invites."; + } + + TWBText.setText(%header @ %text); + } + + // reset the cursor to non-wait mode + Canvas.setCursor(defaultCursor); +} + +// create a live link to a player with their current GUID, name, and tag +function tn_community_browserui_liveLinkPlayer(%player) +{ + // %player format: name \t tag \t append \t guid + %name = getField(%player, 0); + %tag = getField(%player, 1); + %append = getField(%player, 2); + %guid = getField(%player, 3); + + %colorhex = ""; + for (%i = 0; %i <= 2; %i++) + { + %byte = DecToHex(getWord($TribeTagColor, %i)); + while(strlen(%byte) < 2) + %byte = "0" @ %byte; + %colorhex = %colorhex @ %byte; + } + %tag = "" @ %tag; + + %colorhex = ""; + for (%i = 0; %i <= 2; %i++) + { + %byte = DecToHex(getWord($PlayerNameColor, %i)); + while(strlen(%byte) < 2) + %byte = "0" @ %byte; + %colorhex = %colorhex @ %byte; + } + %name = "" @ %name; + + if (%append) + %colored = %name @ %tag; + else + %colored = %tag @ %name; + + return ""@ %colored @ ""; +} + +function tn_community_browserui_liveLinkClan(%clan) +{ + %id = getField(%clan, 0); + %name = getField(%clan, 1); + // tag, append are fields 2/3 respectively, but unused here + + %colorhex = ""; + for (%i = 0; %i <= 2; %i++) + { + %byte = DecToHex(getWord($PlayerNameColor, %i)); + while(strlen(%byte) < 2) + %byte = "0" @ %byte; + %colorhex = %colorhex @ %byte; + } + %name = "" @ %name; + + return "" @ %name @ ""; +} + +// expands a template and fills in details with live links where requred +function tn_community_browserui_expandTemplate(%template, %payload, %player1, %player2, %player, %clan) +{ + %pre = ""; + %val["@player"] = tn_community_browserui_liveLinkPlayer(%player); + %val["@clan"] = tn_community_browserui_liveLinkClan(%clan); + %val["@player1"] = tn_community_browserui_liveLinkPlayer(%player1); + %val["@player2"] = tn_community_browserui_liveLinkPlayer(%player2); + %val["@payload"] = %pre @ %payload @ ""; + %val["@payload^"] = %pre @ (%payload ? "YES" : "NO") @ ""; + + %pcount = getFieldCount(%payload); + for (%i = 0; %i < %pcount; %i++) + { + %field = getField(%payload, %i); + %val["@payload;" @ %i] = %pre @ %field @ ""; + if (%field) + %val["@payload;" @ %i @ "^"] = %pre @ "YES"; + else + %val["@payload;" @ %i @ "^"] = %pre @ "NO"; + } + + %output = ""; + %index = strstr(%template, "@"); + while (%index != -1) + { + // scan ahead for a non alphanum, semicolon, or ^ character + %strlen = strlen(%template); + for (%i = %index; %i < %strlen; %i++) + { + %char = strcmp(getSubStr(%template, %i, 1), ""); + if (!((%char >= 48 && %char <= 57) || (%char >= 97 && %char <= 122) || %char == 64 || %char == 59 || %char == 94)) + break; + } + %lookup = getSubStr(%template, %index, %i - %index); + %value = %val[%lookup]; + + %output = %output @ getSubStr(%template, 0, %index); + %output = %output @ %value; + + %template = getSubStr(%template, %i, %strlen); + %index = strstr(%template, "@"); + } + return %output @ %template; +} + +// ========================================================================= +// Tribe Profile Admin +// ========================================================================= + +// replacing function in webbrowser.cs, 2336 +function TribePropertiesDlg::onWake(%this) +{ + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + + %name = %clan.name; + %tag = getField(%clan.tag, 0); + %append = getField(%clan.tag, 1); + %recru = %clan.recruiting; + %info = %clan.info; + + if(%recru) + TP_RecruitFlagBtn.setValue(1); + else + TP_RecruitFlagNoBtn.setValue(1); + + if(%append) + TP_AppendFlagBtn.setValue(1); + else + TP_PrePendFlagBtn.setValue(1); + + TP_CurrentTag.setText(%tag); + TP_NewTag.setText(%tag); + TP_TribeDescription.setText(%info); + + %this.RefreshTag(); + %this.pendingChanges = ""; + Canvas.setCursor(defaultCursor); + + // add new UI elements to change the clan name + ProfileControl.extent = "385 280"; + %this.getObject(0).setText("Tribe Administration"); + + // move the close button to the top-right and make it an "X" + TP_OKBtn.text = "X"; + TP_OKBtn.extent = "40 38"; + TP_OKBtn.position = "365 1"; + + // update the clan rename field with this clan's name + // store it so we know if we're actually changing it when the rename button is hit + %this.nameField.setText(%name); + %this.nameField.cname = %name; +} + +// this is a new function for TribesNext +function TribePropertiesDlg::RenameTribe(%this) +{ + %field = %this.nameField; + %clanid = TribePane.targetid; + %org = %field.cname; + %new = %field.getValue(); + if (%org $= %new) + { + MessageBoxOK("NO ACTION","Current and new name is the same.",""); + } + else + { + // verify they want to do it + MessageBoxYesNo("CONFIRM", "Are you sure you want to change the clan name from \n\"" @ %org @ "\"\nto\n\"" @ %new @ "\"?", + "tn_community_browser_clan_rename(\"" @ expandEscape(%clanid) @ "\", \"" @ expandEscape(%new) @ "\");",""); + } +} + +// replacing function in webbrowser.cs, 2371 +function TribePropertiesDlg::DisbandTribe(%this) +{ + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + %name = %clan.name; + + MessageBoxYesNo("AUTHORIZE","At least 50% of Rank 4 members must authorize a disband." NL " " NL + "Click YES to authorize, or NO to deauthorize disband of \"" @ %name @ "\".", + "tn_community_browser_clan_disband(\"" @ expandEscape(%clanid) @ "\", 1);", + "tn_community_browser_clan_disband(\"" @ expandEscape(%clanid) @ "\", 0);"); +} + +// replacing function in webbrowser.cs, 2378 +function TribePropertiesDlg::ChangeRecruiting(%this) +{ + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + %recru = %clan.recruiting; + + if(TP_RecruitFlagBtn.getValue()) + %recruiting = 1; + else + %recruiting = 0; + if (%recru != %recruiting) + { + // fire off a request to change the recruiting flag + tn_community_browser_clan_recruiting(%clanid, %recruiting); + + %this.pendingChanges=""; + } +} + +// replacing function in webbrowser.cs, 2400 +function TribePropertiesDlg::ChangeTag(%this) +{ + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + + if(TP_NewTag.getValue() !$= "") + { + %tag = TP_NewTag.getValue(); + %append = TP_AppendFlagBtn.getValue(); + + // fire off request to change tag + tn_community_browser_clan_retag(%clanid, %tag, %append); + } + else + { + MessageBoxOK("WARNING","Tribe Tag cannot be blank","TP_NewTag.makeFirstResponder(1);"); + } +} + +// replacing function in webbrowser.cs, 2391 +function TribePropertiesDlg::ToggleAppending(%this) +{ + %this.RefreshTag(); +} + +// replacing function in webbrowser.cs, 2486 +function TribePropertiesDlg::setTribeGraphic(%this) +{ + %picture = TribeGraphic.bitmap; + TeamPix.setBitmap(%picture); + + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + tn_community_browser_clan_picture(%clanid, %picture); +} + +// replacing function in webbrowser.cs, 2429 +function TribePropertiesDlg::ClearDescription(%this) +{ + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + + MessageBoxYesNo("DESCRIPTION","Are you sure you want to clear the clan info description?", + "tn_community_browser_clan_info(" @ %clanid @ ", \"\");TP_TribeDescription.setText(\"\");",""); +} + +// replacing function in webbrowser.cs, 2423 +function TribePropertiesDlg::EditDescription(%this) +{ + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + %info = %clan.info; + + TWBText.editType = "tribe"; + Canvas.pushDialog(BrowserEditInfoDlg); + EditDescriptionText.setValue(%info); +} + +// replacing function in webbrowser.cs, 199 +function EditDescriptionApply() +{ + %desc = EditDescriptionText.getValue(); + if(TWBText.editType $= "tribe") + { + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + %clan.info = %desc; + + tn_community_browser_clan_info(%clanid, %desc); + } + else + { + tn_community_browser_user_info(%desc); + Canvas.popDialog(BrowserEditInfoDlg); + + PlayerPane.updateActive = 1; + tn_community_browserui_clearCheckStatus(); + + WP_WarriorDescription.setText(%desc); + } +} + +// replacing function in webbrowser.cs, 2187 +function MemberList::onRightMouseDown(%this, %column, %row, %mousePos) +{ + MemberList.setSelectedRow(%row); + %tguid = MemberList.getSelectedId(); + + %clanid = TribePane.targetid; + %clan = tn_community_browser_getClanProfile(%clanid); + + //echo("Right clicked on " @ %tguid @ " in clan " @ %clanid); + + TribeMemberPopup.position = %mousePos; + Canvas.pushDialog(TribeMemberPopupDlg); + TribeMemberPopupDlg.onWake(); + TribeMemberPopup.forceOnAction(); +} + +// replacing function in webbrowser.cs, 2240 +function TribeMemberPopupDlg::onWake(%this) +{ + %tguid = MemberList.getSelectedId(); + %trow = MemberList.getRowTextById(%tguid); + %tname = getField(%trow, 0); + %ttitle = getField(%trow, 1); + %trank = getField(%trow, 2); + %tclanid = getField(%trow, 3); + + %myguid = getField(WONGetAuthInfo(), 3); + + TribeWarriorBrowserGui.TDialogOpen = true; + TribeMemberPopup.clear(); + %isMember = tn_community_browserui_amIMember(%tclanid); + + TribeMemberPopup.add(%tname, -1); + + if (%tguid != %myguid) + { + TribeMemberPopup.add("--------------------------------------------", -1); + TribeMemberPopup.add("Send E-mail", 2); + TribeMemberPopup.add("Add to Buddylist", 3); + TribeMemberPopup.add("Add to Blocklist", 4); + } + + if(%isMember) + { + TribeMemberPopup.add("--------------------------------------------", -1); + + // only show the options if they can be done. + // obviously these are all enforced on the server side too, so this is to minimize error messages shown to users + %myRank = tn_community_browserui_myRankIn(%tclanid); + + if (%tguid == %myguid) + { + // targeting self + TribeMemberPopup.add("Leave Tribe", 0); //can always leave a clan + if (%myRank > 0) // must be at least rank 1 to change title + { + // anyone can downrank themselves if they so choose, however, once they are rank 1 + TribeMemberPopup.add("Edit Rank/Title", 1); + } + else + { + TribeMemberPopup.add("Too low rank to edit own title", -1); + } + } + else + { + // targeting another user + if (%myRank >= 3) + { + // see if the target rank is same/lower + if (%trank < %myRank) + { + TribeMemberPopup.add("Kick from Tribe", 0); + TribeMemberPopup.add("Edit Rank/Title", 1); + } + else + { + TribeMemberPopup.add("Too low rank to kick/edit member", -1); + } + } + else + { + TribeMemberPopup.add("Too low rank to kick/edit member", -1); + } + } + + } + + Canvas.rePaint(); +} + +// replacing function in webbrowser.cs, 2282 +function TribeMemberPopup::onSelect(%this, %id, %text) +{ + %tguid = MemberList.getSelectedId(); + %trow = MemberList.getRowTextById(%tguid); + %tname = getField(%trow, 0); + %ttitle = getField(%trow, 1); + %trank = getField(%trow, 2); + %tclanid = getField(%trow, 3); + %clan = tn_community_browser_getClanProfile(%tclanid); + + %myguid = getField(WONGetAuthInfo(), 3); + + switch( %id ) + { + case 0: // Kick + if (%tguid != %myguid) + { + MessageBoxYesNo("CONFIRM", "Are you sure you want to kick \"\n" @ %tname @ "\" from \"" @ %clan.name @ "\"?", + "tn_community_browser_clan_kick(\"" @ expandEscape(%tclanid) @ "\", \"" @ expandEscape(%tguid) @ "\");", ""); + } + else + { + // targeting self -- make it a leave + MessageBoxYesNo("CONFIRM", "Are you sure that you want to leave \"" @ %clan.name @ "\"?", + "tn_community_browser_user_leaveClan(\"" @ expandEscape(%tclanid) @ "\");", ""); + } + case 1: // Admin Member + LinkEditMember(%trow, TribeAdminMemberDlg); + case 2: // EMail Member + LinkEMail(%tguid); + case 3: // Add To Buddylist + MessageBoxYesNo("CONFIRM","Add \"" @ %tname @ "\" to Buddy List?", + "tn_community_mail_request_addListEntry(\"buddy\", \"" @ expandEscape(%tguid) @ "\");",""); + case 4: // Add To Blocklist + MessageBoxYesNo("CONFIRM","Block Email from \"" @ %tname @ "\"?", + "tn_community_mail_request_addListEntry(\"ignore\", \"" @ expandEscape(%tguid) @ "\");",""); + } + canvas.popDialog(TribeMemberPopupDlg); +} + +// replacing function in webbrowser.cs, 389 +function LinkEditMember(%row, %owner) +{ + %name = getField(%row, 0); + %title = getField(%row, 1); + %rank = getField(%row, 2); + %clanid = getField(%row, 3); + %guid = MemberList.getSelectedId(); + + %clan = tn_community_browser_getClanProfile(%clanid); + + %myguid = getField(WONGetAuthInfo(), 3); + %myRank = tn_community_browserui_myRankIn(%clanid); + + //initialize buttons + %button[0] = tb_onProbation; + %button[1] = tb_tribeMember; + %button[2] = tb_tribeAdmin; + %button[3] = tb_tribeController; + %button[4] = tb_sysAdmin; + + for (%i = 0; %i < 5; %i++) + { + %button[%i].setVisible(true); + %button[%i].setActive(false); + %button[%i].setValue(false); + } + + %owner.vTribe = %clanid; + %owner.vPlayer = %guid; + t_whois.setValue(%name); + E_Title.setValue(%title); + + for (%i = 0; %i <= %myRank; %i++) + { + %button[%i].setActive(true); + } + %button[%rank].setValue(true); + + Canvas.pushDialog(%owner); +} + +// replacing function in webbrowser.cs, 632 +function SetMemberProfile() +{ + if(strLen(trim(E_Title.getValue)) <= 0) + { + %title = E_Title.getValue(); + %rank = TribeAdminMemberDlg.vPerm; + %clanid = TribeAdminMemberDlg.vTribe; + %guid = TribeAdminMemberDlg.vPlayer; + + tn_community_browser_clan_changeRank(%clanid, %guid, %rank, %title); + + Canvas.popDialog(TribeAdminMemberDlg); + + // initiate a UI update for the browser + TribePane.updateActive = 1; + tn_community_browserui_clearCheckStatus(); + } + else + MessageBoxOK("WARNING", "Member Title cannot be blank."); +} + +// ========================================================================= +// Warrior Profile Viewing +// ========================================================================= + +// replacing function in webbrowser.cs, 1864 +function PlayerPane::ButtonClick(%this, %senderid) +{ + canvas.SetCursor(ArrowWaitCursor); + %this.tabstate = "WARRIOR"; + + %myguid = getField(WONGetAuthInfo(), 3); + %guid = TWBTabView.getSelectedId(); + + %this.targetid = %guid; + %this.state = "NONE"; + + %now = getSimTime(); + if (%now < $TribesNext::Community::BrowserUI::MinRefreshTime) + %now = $TribesNext::Community::BrowserUI::MinRefreshTime + 1; + + switch(%senderid) + { + case 0: // Player Profile + %this.state = "VIEW_PLAYER"; + %playerobj = tn_community_browser_getPlayerProfile(%guid); + %delta = %now - %playerobj.lastRefresh; + + if ($TribesNext::Community::BrowserUI::MinRefreshTime < %delta) + { + %this.updateActive = 1; + tn_community_browser_user_view(%guid); + tn_community_browserui_clearCheckStatus(); + } + else + { + tn_community_browserui_showPlayerPane(); + } + case 1: + //Player History + %this.state = "PLAYER_HISTORY"; + %playerobj = tn_community_browser_getPlayerProfile(%guid); + %delta = %now - %playerobj.lastHistRefresh; + + if ($TribesNext::Community::BrowserUI::MinRefreshTime < %delta) + { + %this.updateActive = 1; + %playerobj.lastHistRefresh = %now; + tn_community_browser_user_history(%guid); + tn_community_browserui_clearCheckStatus(); + } + else + { + tn_community_browserui_showPlayerPane(); + } + + case 2: + //TribeList + %this.state = "VIEW_PLAYER"; + W_MemberList.CID = 0; + tn_community_browserui_showPlayerPane(); + case 3: + //Player Buddylist + %this.state = "VIEW_PLAYER"; + W_MemberList.CID = 1; + tn_community_browserui_showPlayerPane(); + case 4: + //Visitor Options + %this.state = "PLAYER_OPTIONS"; + %playerobj = tn_community_browser_getPlayerProfile(%guid); + %delta = %now - %playerobj.lastRefresh; + + if ($TribesNext::Community::BrowserUI::MinRefreshTime < %delta) + { + %this.updateActive = 1; + tn_community_browser_user_view(%guid); + tn_community_browserui_clearCheckStatus(); + } + else + { + tn_community_browserui_showPlayerPane(); + } + case 5: + //Admin Options + WarriorPropertiesDlg.pendingChanges = ""; + Canvas.PushDialog(WarriorPropertiesDlg); + } +} + +function tn_community_browserui_showPlayerPane() +{ + %this = PlayerPane; + %myguid = getField(WONGetAuthInfo(), 3); + %guid = %this.targetid; + %player = tn_community_browser_getPlayerProfile(%guid); + + switch$ (%this.state) + { + case "VIEW_PLAYER": + if (%player.name !$= "") + { + %append = getField(%player.tag, 1); + %titletag = getField(%player.tag, 0); + TWBTitle.setText((%append ? %player.name @ %titletag : %titletag @ %player.name)); + TWBTabView.setTabText(%guid, %player.name); + } + + W_Text.clear(); + + // pretty much a straight copy of the header created by Dynamix -- just activated the previously commented out online status line + %profileText = " \n"; + %profileText = %profileText @ "Registered:" SPC %player.date @ "\n"; + %profileText = %profileText @ "Online: " SPC (%player.online ? "YES":"NO") @ "\n"; + %profileText = %profileText @ "Website: " SPC ""@ %player.site @"\n\n"; + %profileText = %profileText @ ""; + + W_Text.setText(%profileText @ %player.info); + + if (!W_MemberList.CID) + { + // populate the membership data + W_MemberList.Clear(); + W_MemberList.ClearColumns(); + W_MemberList.clearList(); + W_MemberList.addColumn( 0, "TRIBE", 94, 0, 330 ); + W_MemberList.addColumn( 1, "TITLE", 80, 0, 300 ); + W_MemberList.addColumn( 2, "RNK", 38, 0, 50, "numeric center" ); + + %mcount = %player.mcount; + if (%mcount !$= "") + { + %ptag = getField(%player.tag, 0); + for (%i = 0; %i <= %mcount; %i++) + { + %membership = %player.membership[%i]; + %mid = getField(%membership, 0); + %mname = getField(%membership, 1); + %mrank = getField(%membership, 2); + %mtitle = getField(%membership, 3); + %mtag = getField(%membership, 4); + + W_MemberList.addRow(%mid, %mname TAB %mtitle TAB %mrank TAB %mid); + W_MemberList.setRowStylebyID(%mid, (%mtag !$= %ptag)); + } + } + } + else + { + W_MemberList.Clear(); + W_MemberList.ClearColumns(); + W_MemberList.clearList(); + W_MemberList.addColumn( 0, "BUDDY", 212, 0, 250 ); + + // populate buddylist + if ($TMail::ListMax["buddy"] !$= "") + { + %buddycount = $TMail::ListMax["buddy"]; + for (%i = 0; %i <= %buddycount; %i++) + { + %buddy = $TMail::ListVals["buddy", %i]; + %buddyname = getField(%buddy, 0); + %buddyguid = getField(%buddy, 3); + W_MemberList.addRow(%buddyguid, %buddyname); + } + } + } + + // show the player picture (or default if none) + if (%pict !$= "") + { + PlayerPix.setBitmap(%pict); + } + else + { + PlayerPix.schedule(300, "setBitmap", "texticons/twb/twb_Lineup.jpg"); + } + case "PLAYER_HISTORY": + W_Text.clear(); + %header = "\nHistory:\n\n"; + + %text = ""; + %hcount = %player.hcount; + %start = getRealTime(); + if (%hcount !$= "") + { + for (%i = 0; %i <= %hcount; %i++) + { + if (%player.historyCache[%i] $= "") + { + %event = %player.history[%i]; + %etype = getField(%event, 0); + %etime = tn_community_mailui_epochToDate(getField(%event, 1)); + %payload = collapseEscape(getField(%event, 2)); + %template = getField(%event, 3); + %playerv = getFields(%event, 4, 7); + %clan = getFields(%event, 8); + %expanded = tn_community_browserui_expandTemplate(%template, %payload, "", "", %playerv, %clan); + + %line = "" @ %etime @ " " @ %expanded; + %player.historyCache[%i] = %line; + } + else + { + %line = %player.historyCache[%i]; + } + %text = %line @ "\n" @ %text; + } + } + %end = getRealTime(); + //echo("Draw Time: " @ %end - %start); + W_Text.setText(%header @ %text); + case "PLAYER_OPTIONS": + W_Text.clear(); + %text = "\nOptions for " @ %player.name @ ":\n\n" @ + "Visit Website\n"; + + %text = %text @ "Send E-mail\n"; + + // check if on buddy list already and switch this to remove if so + if (tn_community_isUserBuddy(%guid) !$= "") + %text = %text @ "Remove from Buddylist\n"; + else + %text = %text @ "Add to Buddylist\n"; + %text = %text @ "\n"; + + // add invitation links to clans that the current player has invitation ability to, and the target player is not a member of + %self = tn_community_browser_getPlayerProfile(%myguid); + if (%self.mcount !$= "") + { + %idxs = ""; + for (%i = 0; %i <= %self.mcount; %i++) + { + %membership = %self.membership[%i]; + %crank = getField(%membership, 2); + if (%crank >= 2) + %idxs = %idxs @ "\t" @ %i; + } + %idxs = trim(%idxs); + } + %cnt = getFieldCount(%idxs); + for (%i = 0; %i < %cnt; %i++) + { + %thisidx = getField(%idxs, %i); + %checkagainst = getField(%self.membership[%thisidx], 0); + %found = 0; + if (%player.mcount !$= "") + { + for (%j = 0; %j <= %player.mcount; %j++) + { + %checkid = getField(%player.membership[%j], 0); + if (%checkagainst == %checkid) + %found = 1; + } + } + if (!%found) + %outIdx = %outIdx @ "\t" @ %thisidx; + } + %outIdx = trim(%outIdx); + %cnt = getFieldCount(%outIdx); + for (%i = 0; %i < %cnt; %i++) + { + %membership = %self.membership[getField(%outIdx, %i)]; + %clanid = getField(%membership, 0); + %clanname = getField(%membership, 1); + %text = %text @ "Invite " @ %player.name @ " to join \"" @ %clanname @ "\"\n"; + } + + W_Text.setText(%text); + } + + // reset the cursor to non-wait mode + Canvas.setCursor(defaultCursor); +} + +// replacing function in webbrowser.cs, 1851 +function PlayerPane::DblClick(%this) +{ + %id = W_MemberList.getSelectedId(); + %text = getField(W_MemberList.getRowTextById(%id), 0); + + %myguid = getField(WONGetAuthInfo(), 3); + %tabid = TWBTabView.getSelectedId(); + + if(w_buddylist.getValue() && (%myguid == %tabid)) + { + TWBTabView.view(%id, %text); + } + else + { + TWBTabView.view(%id, %text, "Tribe"); + } +} + +// ========================================================================= +// Warrior Profile Admin +// ========================================================================= + +// replacing function in webbrowser.cs, 2002 +function W_MemberList::onRightMouseDown( %this, %column, %row, %mousePos ) +{ + %myguid = getField(WONGetAuthInfo(), 3); + %tabid = TWBTabView.getSelectedId(); + + // Open the action menu: + W_MemberList.setSelectedRow(%row); + if (%myguid == %tabid) + { + %id = W_MemberList.getSelectedId(); + %text = W_MemberList.getRowTextById(%id); + if(w_buddylist.getValue()) + { + // buddylist + WarriorPopup.text = %text; + WarriorPopup.id = %id; + + WarriorPopup.position = %mousePos; + Canvas.pushDialog(WarriorPopupDlg); + WarriorPopUpDlg.onWake(); + WarriorPopup.forceOnAction(); + } + else + { + // clan + WarriorPopup.text = getField(%text, 0); + WarriorPopup.id = getField(%text, 3); + WarriorPopup.position = %mousePos; + Canvas.pushDialog(WarriorPopupDlg); + WarriorPopUpDlg.onWake(); + WarriorPopup.forceOnAction(); + } + } +} + +// replacing function in webbrowser.cs, 2058 +function WarriorPopupDlg::onWake( %this ) +{ + %myguid = getField(WONGetAuthInfo(), 3); + %tabid = TWBTabView.getSelectedId(); + + TribeAndWarriorBrowserGui.WDialogOpen = true; + warriorPopUP.clear(); + if (%myguid == %tabid) + { + switch(W_MemberList.CID) + { + case 0: + + WarriorPopUp.add( WarriorPopup.text, -1); + WarriorPopUp.add( "---------------------------------------------", -1); + WarriorPopup.add( "Clear Primary Tribe setting", 0); + WarriorPopUp.add( "Make Primary Tribe", 1 ); + WarriorPopup.add( "Leave Tribe", 2 ); + + case 1: + WarriorPopUp.add( WarriorPopup.text, -1); + WarriorPopUp.add( "---------------------------------------------", -1); + WarriorPopup.add( "Contact By EMail", 3 ); + WarriorPopup.add( "Remove from Buddylist", 4 ); + WarriorPopup.add( ".............................................", -1); + WarriorPopup.add( "EMail BuddyList", 5 ); + } + } + Canvas.rePaint(); +} + +// replacing function in webbrowser.cs, 2092 +function WarriorPopup::onSelect( %this, %id, %text ) +{ + switch( %id ) + { + case 0: // set active clan tag to none (but retain membership) + MessageBoxYesNo("CONFIRM", "Are you sure you wish to hide your tag?", + "tn_community_browser_user_activeClan(-1); PlayerPane.updateActive = 1; tn_community_browserui_clearCheckStatus();",""); + + case 1: // set active clan tag to one of the clans the user is a member of + MessageBoxYesNo("CONFIRM", "Are you sure you wish to set \n\"" @ %this.text @ "\"\n as your active clan?", + "tn_community_browser_user_activeClan(" @ expandEscape(%this.id) @ "); PlayerPane.updateActive = 1; tn_community_browserui_clearCheckStatus();",""); + + case 2: // leave Tribe + MessageBoxYesNo("CONFIRM", "Are you sure you wish to leave \n\"" @ %this.text @ "\"?", + "tn_community_browser_user_leaveClan(" @ expandEscape(%this.id) @ "); PlayerPane.updateActive = 1; tn_community_browserui_clearCheckStatus();",""); + case 3: // EMail Buddy -- FUTURE this is a little fritzy + %record = WarriorPopup.id TAB WarriorPopup.text; + LinkEMail(%record); + case 4: // Remove Buddy + MessageBoxYesNo("CONFIRM","Remove \"" @ WarriorPopup.text @ "\" from Buddy List?", + "tn_community_mail_request_delListEntry(\"buddy\", \"" @ expandEscape(WarriorPopup.id) @ "\"); PlayerPane.updateActive = 1; schedule(300, 0, tn_community_browserui_clearCheckStatus);",""); + case 5: // TODO EMail Buddylist + %count = w_memberlist.rowCount(); + for(%x = 0; %x < %count; %x++) + { + %mailList = %mailList TAB w_memberList.getRowId(%x); + } + %mailList = trim(%mailList); + error(%mailList); + //LinkEMail(%mailList); + } + canvas.PopDialog(WarriorPopupDlg); +} + +// replacing function in webbrowser.cs, 2507 +function WarriorPropertiesDlg::onWake(%this) +{ + %myguid = getField(WONGetAuthInfo(), 3); + %player = tn_community_browser_getPlayerProfile(%myguid); + + %this.pendingChanges = ""; + UrlEdit.setValue(%player.site); + WP_CurrentName.setValue(%player.name); + NewNameEdit.setValue(""); + WP_WarriorDescription.setText(%player.info); + %this.LoadGfxPane(); +} + +// replacing function in webbrowser.cs, 2531 +function WarriorPropertiesDlg::EditDescription(%this) +{ + %myguid = getField(WONGetAuthInfo(), 3); + %player = tn_community_browser_getPlayerProfile(%myguid); + + TWBText.editType = "player"; + Canvas.pushDialog(BrowserEditInfoDlg); + EditDescriptionText.setValue(%player.info); +} + +// replacing function in webbrowser.cs, 2538 +function WarriorPropertiesDlg::ClearDescription(%this) +{ + MessageBoxYesNo("CONFIRM", "Clear your profile description?", "WarriorPropertiesDlg.doClearDescription();", ""); +} + +// replacing function in webbrowser.cs, 2543 +function WarriorPropertiesDlg::doClearDescription(%this) +{ + %this.pendingChanges = ""; + EditDescriptionText.setText(""); + WP_WarriorDescription.setText(EditDescriptionText.getText()); + + tn_community_browser_user_info(""); + + PlayerPane.updateActive = 1; + tn_community_browserui_clearCheckStatus(); +} + +// replacing function in webbrowser.cs, 2582 +function WarriorPropertiesDlg::setPlayerGraphic(%this) +{ + PlayerPix.setBitmap(PlayerGraphic.bitmap); + %this.pendingChanges = ""; + + tn_community_browser_user_picture(PlayerGraphic.bitmap); + + PlayerPane.updateActive = 1; + tn_community_browserui_clearCheckStatus(); +} + +// replacing function in webbrowser.cs, 2594 +function WarriorPropertiesDlg::UpdateUrl(%this) +{ + if(trim(UrlEdit.getValue()) $= "") + { + UrlEdit.setValue("www.tribesnext.com"); + MessageBoxYesNo("CONFIRM","Your URL is blank, by default www.tribesnext.com will become your URL. Continue?","WarriorPropertiesDlg.setURL();","UrlEdit.setValue(\"\");"); + } + else + WarriorPropertiesDlg.setURL(); +} + +// replacing function in webbrowser.cs, 2606 +function WarriorPropertiesDlg::setURL(%this) +{ + %this.pendingChanges = ""; + %url = UrlEdit.getValue(); + + tn_community_browser_user_website(%url); + + PlayerPane.updateActive = 1; + tn_community_browserui_clearCheckStatus(); +} + +// replacing function in webbrowser.cs, 2617 +function WarriorPropertiesDlg::ChangePlayerName(%this) +{ + MessageBoxYesNo("CONFIRM", "Are you sure you want to change your player name?", "WarriorPropertiesDlg.ProcessNameChange();", "NewNameEdit.setValue(\"\");"); +} + +// replacing function in webbrowser.cs, 2623 +function WarriorPropertiesDlg::ProcessNameChange(%this) +{ + %this.pendingChanges = ""; + %name = NewNameEdit.getValue(); + + tn_community_browser_user_rename(%name); + + PlayerPane.updateActive = 1; + tn_community_browserui_clearCheckStatus(); +} + +// replacing function in webbrowser.cs, 2631 +function WarriorGraphicsList::onSelect(%this) +{ + %jpg = "texticons/twb/" @ %this.getRowText(%this.getSelectedRow()) @ ".jpg"; + PlayerGraphic.setBitmap(%jpg); +} + +// ========================================================================= +// "CREATE TRIBE" +// ========================================================================= + +// replacing function in webbrowser.cs, 110 +function CreateTribe() +{ + $CreateTribeName = ""; + $CreateTribeTag = ""; + $CreateTribeAppend = true; + $CreateTribeRecruiting = true; + + if (isObject(CreateTribeDlg)) + CreateTribeDlg.delete(); + + LoadGui(CreateTribeDlg); + + // modify the UI here, since it is reloaded every time it is needed + CT_TagText.maxLength = 12; // max tag length = 12 + CT_TagText.IRCName = 0; // disables wierd/wrong tag text validation + CreateTribeDlg.getObject(0).getObject(3).maxLength = 40; // max length of clan name + CreateTribeDlg.getObject(0).getObject(3).IRCName = 0; // disable wierd/wrong tag name validation + + Canvas.pushDialog( CreateTribeDlg ); +} + +// replacing function in webbrowser.cs, 124 +function CreateTribeProcess() +{ + %name = trim($CreateTribeName); + if (strlen(%name) == 0) + { + MessageBoxOK("WARNING", "Tribe Name cannot be blank."); + return; + } + %tag = $CreateTribeTag; + if (strlen(%tag) == 0) + { + MessageBoxOK("WARNING", "Tribe Tag cannot be blank."); + return; + } + %append = $CreateTribeAppend; + %recru = $CreateTribeRecruiting; + %info = CreateTribeDescription.getText(); + + // send the creation request + tn_community_browser_user_createClan(%tag, %append, %name, %info, %recru); +} \ No newline at end of file diff --git a/t2csri/community/login.cs b/t2csri/community/login.cs new file mode 100644 index 0000000..95dc2a8 --- /dev/null +++ b/t2csri/community/login.cs @@ -0,0 +1,185 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2011 + +// Tribes 2 Community System +// Robot Session Client + +// Since the game itself does not store the users' passwords for any longer than is required +// to decrypt their RSA private keys, the "robot" client must negotiate sessions through an +// RSA challenge/response. + +// The robot client issues a challenge request by sending the user's GUID and a random nonce. +// The DCE issues a challenge that is encrypted with the user's public key. The challenge is +// valid for a server configured lifetime, during which any challenge request by the same GUID +// would return the same challenge. The client sends the decrypted challenge back to the DCE, and +// if it is a match, a session is initiated, and a session UUID is returned to the robot client, +// which it uses to verify its identity for all authenticated requests. The challenge lifetime is +// sufficiently generous to allow an RSA decryption and heavy network latency. + +// The client will refresh periodically (every 10 minutes by default) to keep the session alive. + +function CommunitySessionInterface::onLine(%this, %line) +{ + //warn("SInterf: " @ %line); + if (trim(%line) $= "") + { + %this.primed = 1; + return; + } + if (%this.primed) + { + echo(%line); + if (getSubStr(%line, 0, 11) $= "CHALLENGE: ") + { + $TribesNext::Community::SessionErrors = 0; + $TribesNext::Community::Challenge = getSubStr(%line, 11, strlen(%line)); + //error("Challenge set: " @ $TribesNext::Community::Challenge); + + cancel($TribesNext::Community::SessionSchedule); + $TribesNext::Community::SessionSchedule = schedule(200, 0, tn_community_login_initiate); + } + else if (getSubStr(%line, 0, 6) $= "UUID: ") + { + $TribesNext::Community::SessionErrors = 0; + $TribesNext::Community::UUID = getSubStr(%line, 6, strlen(%line)); + $TribesNext::Community::Challenge = ""; + //error("UUID set: " @ $TribesNext::Community::UUID); + + cancel($TribesNext::Community::SessionSchedule); + $TribesNext::Community::SessionSchedule = schedule($TribesNext::Community::SessionRefresh * 1000, 0, tn_community_login_initiate); + } + else if (getSubStr(%line, 0, 5) $= "ERR: ") + { + error("Session negotiation error: " @ getSubStr(%line, 5, strlen(%line))); + $TribesNext::Community::UUID = ""; + $TribesNext::Community::Challenge = ""; + + // add schedule with backoff, up to about 15 minutes + $TribesNext::Community::SessionErrors++; + if ($TribesNext::Community::SessionErrors > 66) + $TribesNext::Community::SessionErrors = 66; + $TribesNext::Community::SessionSchedule = schedule(200 * ($TribesNext::Community::SessionErrors * $TribesNext::Community::SessionErrors), 0, tn_community_login_initiate); + } + else if (getSubStr(%line, 0, 9) $= "REFRESHED") + { + $TribesNext::Community::SessionErrors = 0; + //error("Session refreshed. Scheduling next ping."); + + cancel($TribesNext::Community::SessionSchedule); + $TribesNext::Community::SessionSchedule = schedule($TribesNext::Community::SessionRefresh * 1000, 0, tn_community_login_initiate); + } + else if (getSubStr(%line, 0, 7) $= "TIMEOUT") + { + $TribesNext::Community::SessionErrors = 0; + //error("Session timed out. Refreshing."); + $TribesNext::Community::UUID = ""; + $TribesNext::Community::Challenge = ""; + + cancel($TribesNext::Community::SessionSchedule); + $TribesNext::Community::SessionSchedule = schedule(200, 0, tn_community_login_initiate); + } + } +} + +function CommunitySessionInterface::onConnected(%this) +{ + //echo("Sending: " @ %this.data); + %this.primed = 0; + %this.send(%this.data); +} + +// initiates the session negotiation process +function tn_community_login_initiate() +{ + if (isEventPending($TribesNext::Community::SessionSchedule)) + { + cancel($TribesNext::Community::SessionSchedule); + } + %payload = "GET " @ $TribesNext::Community::BaseURL @ $TribesNext::Community::LoginScript @ "?guid=" @ getField($LoginCertificate, 1) @ "&"; + // is there an existing session? + if ($TribesNext::Community::UUID !$= "") + { + // try to refresh it + %payload = %payload @ "uuid=" @ $TribesNext::Community::UUID; + } + else + { + // no session -- either expired, or never had one + + // is a challenge present + if ($TribesNext::Community::Challenge $= "") + { + // no challenge present... ask for one: + // create a random nonce half of the length of the active RSA key modulus + %length = strlen(getField($LoginCertificate, 3)) / 2; + %nonce = "1"; // start with a one to prevent truncation issues + for (%i = 1; %i < %length; %i++) + { + %nibble = getRandom(0, 15); + if (%nibble == 10) + %nibble = "a"; + else if (%nibble == 11) + %nibble = "b"; + else if (%nibble == 12) + %nibble = "c"; + else if (%nibble == 13) + %nibble = "d"; + else if (%nibble == 14) + %nibble = "e"; + else if (%nibble >= 15) + %nibble = "f"; + %nonce = %nonce @ %nibble; + } + $TribesNext::Community::Nonce = %nonce; + // transmit the request to the community server + %payload = %payload @ "nonce=" @ %nonce; + } + else + { + %challenge = strlwr($TribesNext::Community::Challenge); + for (%i = 0; %i < strlen(%challenge); %i++) + { + %char = strcmp(getSubStr(%challenge, %i, 1), ""); + if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97)) + { + // non-hex characters in the challenge! + error("TNCommunity: Hostile challenge payload returned by server!"); + $TribesNext::Community::Challenge = ""; + tn_community_login_initiate(); + return; + } + } + + // challenge is present... decrypt it and transmit it to the community server + rubyEval("tsEval '$decryptedChallenge=\"' + $accountKey.decrypt('" @ %challenge @ "'.to_i(16)).to_s(16) + '\";'"); + + %verifiedNonce = getSubStr($decryptedChallenge, 0, strLen($TribesNext::Community::Nonce)); + if (%verifiedNonce !$= $TribesNext::Community::Nonce) + { + // this is not the nonce we sent to the community server, try again + error("TNCommunity: Unmatched nonce in challenge returned by server!"); + $TribesNext::Community::Challenge = ""; + tn_community_login_initiate(); + return; + } + else + { + %response = getSubStr($decryptedChallenge, strLen($TribesNext::Community::Nonce), strlen($decryptedChallenge)); + %payload = %payload @ "response=" @ %response; + } + } + } + %payload = %payload @ " HTTP/1.1\r\nHost: " @ $TribesNext::Community::Host @ "\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + + if (isObject(CommunitySessionInterface)) + { + CommunitySessionInterface.disconnect(); + } + else + { + new TCPObject(CommunitySessionInterface); + } + CommunitySessionInterface.data = %payload; + CommunitySessionInterface.connect($TribesNext::Community::Host @ ":" @ $TribesNext::Community::Port); +} \ No newline at end of file diff --git a/t2csri/community/mail.cs b/t2csri/community/mail.cs new file mode 100644 index 0000000..8727efc --- /dev/null +++ b/t2csri/community/mail.cs @@ -0,0 +1,449 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2011-2013 + +// Tribes 2 Community System +// Robot Mail Client + +// This script implements a network data interface to the TribesNext community system mail robot data interface. +// The "robot" data interface provides the data in a way that is easy to parse with the meager and medicore +// string processing and parsing features present in the Tribes 2 game. If you are reading this script and desire +// to make some sort of third party client for web access or other purposes, you will have a much easier time +// if you use the JSON API to access the same data. + +// Currently available methods (as of RC3) are as follow: +// - Viewing the inbox. +// - Viewing the sentbox. +// - Viewing the deleted messages box. +// - Viewing messages. +// - Viewing ignore list. +// - Viewing buddy list. +// - Adding users to an ignore list. +// - Adding users to a buddy list. +// - Deleting users from an ignore list. +// - Deleting users from a buddy list. +// - Deleting (and undeleting) messages. +// - Getting a message count (both read and unread). +// - Sending messages. + +// Since the API is asynchronous, this interface will cache results to the various inboxes and viewed +// messages for the purposes of display. Temporary data (elipses) will be provided to the drawing code +// until all fields are filled in. + +$TribesNext::Community::Mail::Active = 0; +$TribesNext::Community::Mail::ChunkSize = 25; + +function CommunityMailInterface::onConnected(%this) +{ + echo("Sending: " @ %this.data); + %this.primed = 0; + %this.send(%this.data); +} + +function CommunityMailInterface::onDisconnect(%this) +{ + $TribesNext::Community::Mail::Active = 0; + tn_community_mail_executeNextRequest(); +} + +function CommunityMailInterface::onLine(%this, %line) +{ + if (trim(%line) $= "") + { + %this.primed = 1; + return; + } + if (!%this.primed) + return; + + warn("mail: " @ %line); + %message = getField(%line, 0); + switch$ (%message) + { + // display errors to the user -- some of these should never actually happen + case "ERR": + if (getField(%line, 1) $= "MAIL") + { + %type = getField(%line, 2); + switch$ (%type) + { + case "INVALID_RECIP": + %message = "Invalid recipient in mail send request."; + case "INVALID_SBJ": + %message = "Blank or invalid subject in mail send request."; + case "INVALID_BODY": + %message = "Blank or invalid body in message send request."; + case "UNAUTHENTICATED": + %message = "Session authentication error in mail request."; + case "NO_METHOD": + %message = "Internal error: no mail method specified in request."; + case "UNKNOWN_METHOD": + %message = "Internal error: unknown mail method specified in request."; + case "READ": + %message = "Access denied on message ID #" @ getField(%line, 3) @ "."; + default: + %message = "Unknown error in mail system: " @ %line; + } + schedule(500, 0, MessageBoxOK, "ERROR", %message); + } + // success is sent when a message is sent out + case "SUCCESS": + schedule(500, 0, MessageBoxOK, "SENT", "Your message has been sent."); + + // the rest of these should be handled and accepted quietly to populate the various data objects + + // message format sent as part of a box search + case "MSG": + %msg = tn_community_mail_getMessageObject(getField(%line, 1)); + %msg.box = getField(%line, 2); + %msg.read = getField(%line, 3); + %msg.type = getField(%line, 4); + %msg.time = getField(%line, 5); + + %box = tn_community_mail_getMailboxObject(%msg.box); + if (!%box.isMember(%msg)) + { + if (%box.newest < %msg.id) + %box.newest = %msg.id; + %box.add(%msg); + } + + // check if we're getting new messages + if (%box.gettingNew) + { + %since = %box.since; + if (%msg.id <= %since) + { + // found the desired message + %box.gettingNew = 0; + %box.since = %box.newest; + } + else + { + // not yet found desired message, try the next chunk + + // first make sure that the chunk exists and we're not at the end of the mailbox + %box.chunk = %box.chunk + 1; + if ($TMail::MessageBoxCount[%box.name] > (%box.chunk * $TribesNext::Community::Mail::ChunkSize)) + tn_community_mail_request_boxList(%box.chunk * $TribesNext::Community::Mail::ChunkSize, (%box.chunk + 1) * $TribesNext::Community::Mail::ChunkSize, %box.name, %since); + else + { + %box.since = %box.newest; + } + } + } + // message format sent as part of a message view + case "MSG2": + %msg = tn_community_mail_getMessageObject(getField(%line, 1)); + %msg.deleted = getField(%line, 2); + %msg.type = getField(%line, 3); + %msg.time = getField(%line, 4); + %msg.read = "true"; + // message subject + case "SBJ": + tn_community_mail_getMessageObject(getField(%line, 1)).subject = getField(%line, 2); + // sender of a message + case "SNDR": + tn_community_mail_getMessageObject(getField(%line, 1)).sender = tn_community_util_extractPlayer(%line, 2); + // body of a message + case "BDY": + tn_community_mail_getMessageObject(getField(%line, 1)).body = collapseEscape(getField(%line, 2)); + // "to" recipient of a message + case "TO": + %msg = tn_community_mail_getMessageObject(getField(%line, 1)); + %index = getField(%line, 2); + %msg.to[%index] = tn_community_util_extractPlayer(%line, 3); + if (%msg.toMax < %index) + %msg.toMax = %index; + // "cc" recipient of a message + case "CC": + %msg = tn_community_mail_getMessageObject(getField(%line, 1)); + %index = getField(%line, 2); + %msg.cc[%index] = tn_community_util_extractPlayer(%line, 3); + if (%msg.ccMax < %index) + %msg.ccMax = %index; + // entries of a buddy or ignore list + case "LIST": + $TMail::ListVals[getField(%line, 1), getField(%line, 2)] = tn_community_util_extractPlayer(%line, 3); + if ($TMail::ListMax[getField(%line, 1)] < getField(%line, 2)) + $TMail::ListMax[getField(%line, 1)] = getField(%line, 2); + // search results for player name queries + case "SEARCH": + $TMail::SearchVals[getField(%line, 2)] = tn_community_util_extractPlayer(%line, 3); + if ($TMail::SearchMax < getField(%line, 2)) + $TMail::SearchMax = getField(%line, 2); + // unread message count for a box + case "COUNT_U": + $TMail::MessageBoxUnread[getField(%line, 1)] = getField(%line, 2); + // message count for a box + case "COUNT_A": + $TMail::MessageBoxCount[getField(%line, 1)] = getField(%line, 2); + } +} + +// extract four fields from a string that correspond to a player +function tn_community_util_extractPlayer(%string, %fInit) +{ + return getField(%string, %fInit) @ "\t" @ getField(%string, %fInit + 1) @ "\t" @ getField(%string, %fInit + 2) @ "\t" @ getField(%string, %fInit + 3); +} + +function tn_community_mail_getMessageObject(%id) +{ + if (isObject($TMail::MessageTable[%id])) + return $TMail::MessageTable[%id]; + + %obj = new SimObject() + { + class = TMailMessage; + id = %id; + }; + $TMail::MessageTable[%id] = %obj; + + $TMailMessageSet.add(%obj); + return %obj; +} + +function tn_community_mail_getMailboxObject(%name) +{ + if (isObject($TMail::MailboxTable[%name])) + return $TMail::MailboxTable[%name]; + + %obj = new SimSet() + { + class = TMailBox; + name = %name; + since = 0; + }; + $TMail::MailboxTable[%name] = %obj; + return %obj; +} + +function tn_community_mail_initMessageSet() +{ + if (isObject($TMailMessageSet)) + { + while ($TMailMessageSet.getCount() > 0) + $TMailMessageSet.getObject(0).delete(); + $TMailMessageSet.delete(); + } + $TMailMessageSet = new SimSet("TMailMessageSet"); +} +tn_community_mail_initMessageSet(); + +function tn_community_mail_initQueue() +{ + if (isObject($TMailRequestQueue)) + $TMailRequestQueue.delete(); + $TMailRequestQueue = new MessageVector(); +} +tn_community_mail_initQueue(); + +function tn_community_mail_processRequest(%request, %payload) +{ + if (%request !$= "") + { + %request = "?guid=" @ getField($LoginCertificate, 1) @ "&uuid=" @ $TribesNext::Community::UUID @ "&" @ %request; + } + if (%payload $= "") + { + %data = "GET " @ $TribesNext::Community::BaseURL @ $TribesNext::Community::MailScript @ %request; + %data = %data @ " HTTP/1.1\r\nHost: " @ $TribesNext::Community::Host @ "\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + } + else + { + %data = "POST " @ $TribesNext::Community::BaseURL @ $TribesNext::Community::MailScript @ " HTTP/1.1\r\n"; + %data = %data @ "Host: " @ $TribesNext::Community::Host @ "\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n"; + %data = %data @ %payload; + } + + $TMailRequestQueue.pushBackLine(%data); + + if (!$TribesNext::Community::Mail::Active) + tn_community_mail_executeNextRequest(); +} + +function tn_community_mail_executeNextRequest() +{ + if ($TMailRequestQueue.getNumLines() <= 0) + return; + + %data = $TMailRequestQueue.getLineText(0); + $TMailRequestQueue.popFrontLine(); + + $TribesNext::Community::Mail::Active = 1; + + if (isObject(CommunityMailInterface)) + { + CommunityMailInterface.disconnect(); + } + else + { + new TCPObject(CommunityMailInterface); + } + CommunityMailInterface.data = %data; + CommunityMailInterface.connect($TribesNext::Community::Host @ ":" @ $TribesNext::Community::Port); +} + +// implementation of API requests + +// this isn't strictly an API request -- this gets the latest messages since the last check +function tn_community_mail_request_getNew(%box) +{ + %obj = tn_community_mail_getMailboxObject(%box); + tn_community_mail_request_count(%box, "all"); + %since = %obj.since; + %obj.gettingNew = 1; + %obj.chunk = 0; + tn_community_mail_request_boxList(0, $TribesNext::Community::Mail::ChunkSize, %box, %since); +} + +function tn_community_mail_request_boxList(%first, %last, %box, %since) +{ + tn_community_mail_processRequest("method=box&first=" @ %first @ "&last=" @ %last @ "&box=" @ %box @ "&since=" @ %since); +} + +function tn_community_mail_request_read(%messageId) +{ + tn_community_mail_processRequest("method=read&id=" @ %messageId); +} + +function tn_community_mail_request_viewList(%list) +{ + $TMail::ListMax[%list] = 0; + deleteVariables("$TMail::ListVals" @ %list @ "*"); + tn_community_mail_processRequest("method=viewlist&list=" @ %list); +} + +function tn_community_mail_request_addListEntry(%list, %target) +{ + tn_community_mail_processRequest("method=addlist&list=" @ %list @ "&target=" @ %target); + tn_community_mail_request_viewList(%list); // refresh the list +} + +function tn_community_mail_request_delListEntry(%list, %target) +{ + tn_community_mail_processRequest("method=dellist&list=" @ %list @ "&target=" @ %target); + tn_community_mail_request_viewList(%list); // refresh the list +} + +function tn_community_mail_request_deleteMessage(%messageId, %set) +{ + %msg = tn_community_mail_getMessageObject(%messageId); + if (%set $= "0") + { + %add = "&set=0"; + %msg.deleted = "false"; + } + else + { + %add = "&set=1"; + %msg.deleted = "true"; + } + tn_community_mail_processRequest("method=delete&id=" @ %messageId @ %add); + tn_community_mail_request_read(%messageId); // refresh the message status + + // move the message to the right box + if (%set !$= "0") + { + // been deleted, make sure it's in the deleted set + %box = tn_community_mail_getMailboxObject(%msg.box); + %box.remove(%msg); + tn_community_mail_getMailboxObject("deleted").add(%msg); + %msg.box = "deleted"; + } + else + { + // been undeleted? make sure it's not in the deleted set + tn_community_mail_getMailboxObject("deleted").remove(%msg); + if (getField(%msg.sender, 3) !$= getField($LoginCertificate, 1)) + %box = tn_community_mail_getMailboxObject("inbox"); + else + %box = tn_community_mail_getMailboxObject("sentbox"); + %box.add(%msg); + %msg.box = %box.name; + } +} + +function tn_community_mail_request_count(%box, %mode) +{ + tn_community_mail_processRequest("method=count&box=" @ %box @ "&mode=" @ %mode); +} + +function tn_community_mail_request_search(%query) +{ + $TMail::SearchMax = 0; + deleteVariables("$TMail::SearchVals*"); + tn_community_mail_processRequest("method=search&query=" @ %query); +} + +function tn_community_mail_request_send(%subject, %contents, %to, %cc) +{ + // sending messages themselves is done with a POST, + // since the contents can be longer than URI length limits + %guid = getField($LoginCertificate, 1); + %uuid = $TribesNext::Community::UUID; + + %boundary = "-------------------------"; + %rand = getRandom(10000, 99999) @ getRandom(10000, 99999) @ getRandom(10, 9999); + %formelem = "Content-Disposition: form-data; name=\""; + + %payload = "--" @ %boundary @ %rand @ "\r\n"; + + // GUID element + %payload = %payload @ %formelem @ "guid\"\r\n\r\n" @ %guid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // UUID + %payload = %payload @ %formelem @ "uuid\"\r\n\r\n" @ %uuid @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // method + %payload = %payload @ %formelem @ "method\"\r\n\r\nsend\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // subject + %payload = %payload @ %formelem @ "subject\"\r\n\r\n" @ %subject @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // contents + %payload = %payload @ %formelem @ "contents\"\r\n\r\n" @ %contents @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // to + %payload = %payload @ %formelem @ "to\"\r\n\r\n" @ %to @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + // cc + %payload = %payload @ %formelem @ "cc\"\r\n\r\n" @ %cc @ "\r\n"; + %payload = %payload @ "--" @ %boundary @ %rand @ "\r\n"; + + %header = "Content-Type: multipart/form-data; boundary=" @ %boundary @ %rand @ "\r\n"; + %header = %header @ "Content-Length: " @ strlen(%payload) @ "\r\n\r\n"; + + tn_community_mail_processRequest("", %header @ %payload); +} + +function tn_community_isOnList(%searchguid, %list) +{ + if ($TMail::ListMax[%list] $= "") + return ""; + %count = $TMail::ListMax[%list]; + for (%i = 0; %i <= %count; %i++) + { + %player = $TMail::ListVals[%list, %i]; + %guid = getField(%player, 3); + if (%guid == %searchguid) + return %player; + } + return ""; +} + +function tn_community_isUserBuddy(%searchguid) +{ + return tn_community_isOnList(%searchguid, "buddy"); +} + +function tn_community_isUserBlocked(%searchguid) +{ + return tn_community_isOnList(%searchguid, "ignore"); +} diff --git a/t2csri/community/mailUI.cs b/t2csri/community/mailUI.cs new file mode 100644 index 0000000..78384bc --- /dev/null +++ b/t2csri/community/mailUI.cs @@ -0,0 +1,971 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2011-2013 + +// Tribes 2 Community System +// Mail UI Coercion + +// This script implements connectivity between the Dynamix mail UI shipped with Tribes 2 and the community +// systems developed for TribesNext. The communication to the TribesNext systems via network is implemented +// in the robot client data interface script for mail. This script merely connects (modified) Dynamix UI +// elements to query/invoke methods on this new data interface, instead of the IRC server command used +// initially. + +// Several functional changes were made as part of this process. Firstly, all players are now keyed by +// GUID instead of player name. Since player names are not immutable, it is foolish to use them in ways +// that assume they are. As a result of this change, the To/CC fields in the mail composition system do +// not accept input directly from a typing user. If there was a reasonable capability to implement auto +// completion, it may still have been possible to use this UI element. Instead, users will need to press +// the associated To/CC buttons to invoke the address book. From here, they can perform search by name, +// see their buddy list, see fellow members of their clans, and add players to the message (or buddy list). + +// Second, deleted messages can now be undeleted for a duration. Messages marked as deleted are swept by the +// remote community system only at an interval. Users can undelete messages until this deletion process is +// run on the server. Users should not rely on the continued availability of deleted messages, since this +// sweep process can be run at any time. + +// Thirdly, there were some hidden user interface elements that were intended for additional functionality, +// but apparently were never started by Dynamix. This includes a "Sent Item" view, which consists of messages +// sent by this user to other players. This has now been implemented, and connected to the previously hidden +// user interface widgets that were present before. + +// Finally, the original Dynamix code would store mail messages in a local "webcache" file. Since the data +// used to populate these UIs now comes from another script data source, whose format completely differs from +// the original, and since the data format parsing facilities of the game are incredibly primitive, there is +// no longer any local file cache of mail messages. + +// TODO add scroll handler to load more messages + +$TribesNext::Community::MailUI::ActiveMailbox = "inbox"; +$TribesNext::Community::MailUI::ActiveRow = -1; +$TribesNext::Community::MailUI::Awake = 0; + +// avoid sending garbage to the IRC server, just in case code is missed in the UI +function DatabaseQuery(%a0, %a1, %a2, %a3) +{ + error("Uncaught DatabaseQuery(" @ %a0 @ ", " @ %a1 @ ", " @ %a2 @ ", " @ %a3 @ ")"); +} + +function DatabaseQueryArray(%a0, %a1, %a2, %a3) +{ + error("Uncaught DatabaseQueryArray(" @ %a0 @ ", " @ %a1 @ ", " @ %a2 @ ", " @ %a3 @ ")"); +} + +function DatabaseQueryCancel(%a0, %a1, %a2, %a3) +{ + error("Uncaught DatabaseQueryCancel(" @ %a0 @ ", " @ %a1 @ ", " @ %a2 @ ", " @ %a3 @ ")"); +} + +function DatabaseQueryi(%a0, %a1, %a2, %a3) +{ + error("Uncaught DatabaseQueryi(" @ %a0 @ ", " @ %a1 @ ", " @ %a2 @ ", " @ %a3 @ ")"); +} + +// this function makes some (minor) changes to the Dynamix UI structure +function tn_community_mailui_modifyGUIData() +{ + // this UI element is present in the original UI, but it looks like it was never used + rbSendItems.command = "EMailGui.ButtonClick(2);"; + rbSendItems.setVisible(1); + + // expand the delete button so that the text "UNDELETE" can be set comfortably + EM_DeleteBtn.extent = "78 35"; + %shift = 13; + EM_BlockEditBtn.position = (getWord(EM_DeleteBtn.position, 0) + 49 + %shift) SPC 42; + EM_BlockBtn.position = (getWord(EM_DeleteBtn.position, 0) + 123 + %shift) SPC 42; + + // compose window settings changes + EMail_Subject.maxLength = 250; + + // compose window -- making these effectively read only, the backing store must change + EMail_ToEdit.maxLength = 0; + EMail_ToEdit.validate = "tn_community_mailui_recipientValidate();"; + EMail_CcEdit.maxLength = 0; + EMail_CcEdit.validate = "tn_community_mailui_recipientValidate();"; + + // block list window settings, remove the unused UI element indicating number of blocks + %panel = EmailBlockDlg.getObject(0); + if (%panel.getCount() == 5) + %panel.getObject(4).setText(""); + +} +tn_community_mailui_modifyGUIData(); + +// convert from data interface mail structure to the string expected by the Dynamix UI +function tn_community_mailui_convertMessage(%msg) +{ + %out = %msg.id @ "\n"; // id + %out = %out @ %msg.sender @ "\n"; // from + %out = %out @ (%msg.read $= "true") @ "\n"; // read flag + %out = %out @ tn_community_mailui_epochToDate(%msg.time) @ "\n"; // send date + + if (%msg.body !$= "") // mail is loaded + { + // to, always at least one + %count = %msg.toMax; + for (%i = 0; %i <= %count; %i++) + %to = %to @ "\t" @ %msg.to[%i]; + %to = getSubStr(%to, 1, strlen(%to)); + if (%msg.ccMax !$= "") // CC, if exists + { + %count = %msg.ccMax; + for (%i = 0; %i <= %count; %i++) + %cc = %cc @ "\t" @ %msg.cc[%i]; + %cc = getSubStr(%cc, 1, strlen(%cc)); + } + %out = %out @ %to @ "\n" @ %cc @ "\n"; + %out = %out @ %msg.subject @ "\n"; + %out = %out @ %msg.body; + } + else + { + // don't have this message actually downloaded + %out = %out @ "\n\n"; + %out = %out @ %msg.subject @ "\n"; + %out = %out @ "Loading message. Please wait..."; + } +} + +// produces a list of recipients suitable for UI display +function tn_community_mailui_recipientShowList(%list, %color) +{ + %entries = getFieldCount(%list); + %showList = ""; + for (%i = 0; %i < %entries; %i += 4) + { + %name = getField(%list, %i); + %tag = getField(%list, %i + 1); + %append = getField(%list, %i + 2); + %guid = getField(%list, %i + 3); + + if (%color) + { + %name = "\c0" @ %name @ "\c3"; + %tag = "\c2" @ %tag @ "\c3"; + } + + %showName = (%append ? (%name @ %tag) : (%tag @ %name)); + %showList = %showList @ ", " @ %showName; + } + return getSubStr(%showList, 2, strlen(%showList)); +} + +function tn_community_mailui_recipientValidate() +{ + Email_ToEdit.setText(tn_community_mailui_recipientShowList(Email_ToEdit.backing, 1)); + Email_CCEdit.setText(tn_community_mailui_recipientShowList(Email_CCEdit.backing, 1)); +} + +function tn_community_mailui_epochToDate(%epoch) +{ + // uses ruby, since T2 does not expose local system timezone, nor can T2 handle epoch times + + // verify %epoch uses only numbers for security reasons + %len = strlen(%epoch); + for (%i = 0; %i < %len; %i++) + { + %char = strcmp(getSubStr(%epoch, %i, 1), ""); + if (%char > 0x39 || %char < 0x30) + { + %epoch = 0; + break; + } + } + + // check memoization table, since ruby computations can be on the expensive side + %mem = $EpochMem[%epoch]; + if (%mem !$= "") + return %mem; + + // pass it to ruby + $temp = "PROCESSING ERROR"; + rubyEval("tsEval '$temp=\"' + Time.at(" @ %epoch @ ").strftime('%Y-%m-%d %H:%M:%S') + '\";'"); + + // memoize then return + $EpochMem[%epoch] = $temp; + return $temp; +} + +function tn_community_mailui_clearCheckStatus() +{ + if (isEventPending($TribesNext::Community::MailUi::StatusSchedule)) + cancel($TribesNext::Community::MailUi::StatusSchedule); + + if ($TribesNext::Community::Mail::Active) + { + $TribesNext::Community::MailUi::StatusSchedule = schedule(32, 0, tn_community_mailui_clearCheckStatus); + return; + } + EmailGui.checkingEmail = 0; + + $TribesNext::Community::MailUI::ActiveRow = EM_Browser.getSelectedId(); + tn_community_mailui_displayBox($TribesNext::Community::MailUI::ActiveMailbox); + + if (EmailBlockDlg.gettingList) + { + tn_community_mailui_displayBlockList(); + EmailBlockDlg.gettingList = 0; + } + if (AddressDlg.searchActive) + { + tn_community_mailui_displaySearchResults(); + AddressDlg.searchActive = 0; + } + error("Mail UI update."); +} + +function tn_community_mailui_displayBox(%mailbox) +{ + EmailMessageVector.clear(); + EM_Browser.clearList(); + $EmailNextSeq = 0; + EMailInboxBodyText.setText(""); + + if (%mailbox $= "deleted") // set delete button to undelete mode in deleted items + EM_DeleteBtn.text = "UNDELETE"; + else + EM_DeleteBtn.text = "DELETE"; + + %box = tn_community_mail_getMailboxObject(%mailbox); + %count = %box.getCount(); + for (%i = 0; %i < %count; %i++) + { + %msg = %box.getObject(%i); + %id = %msg.id; + EmailNewMessageArrived(tn_community_mailui_convertMessage(%msg), %id); + } + EM_Browser.selectRowByID($TribesNext::Community::MailUI::ActiveRow); + + EM_Browser.sort(); +} + +function tn_community_mailui_loadSelected() +{ + if (isEventPending($TribesNext::Community::MailUi::SelectLoadSchedule)) + cancel($TribesNext::Community::MailUi::SelectLoadSchedule); + + if ($TribesNext::Community::MailUI::Awake) + { + %id = EM_Browser.getSelectedId(); + if (%id != -1) + { + %msg = tn_community_mail_getMessageObject(%id); + if (%msg.body $= "" && !%msg.uiRequested) + { + %msg.uiRequested = 1; + tn_community_mail_request_read(%id); + tn_community_mailui_clearCheckStatus(); + } + } + $TribesNext::Community::MailUi::SelectLoadSchedule = schedule(250, 0, tn_community_mailui_loadSelected); + } +} + +// replacing function in webbrowser.cs, 571 +function LinkEMail(%MailTo) +{ + %count = getRecordCount(%MailTo); + %recipients = ""; + for (%i = 0; %i < %count; %i++) + { + %record = getRecord(%MailTo, %i); + %guid = getField(%record, 0); + %name = getField(%record, 1); + %tag = getField(%record, 2); + %append = getField(%record, 3); + + %player = tn_community_browser_getPlayerProfile(%guid); + if (%name $= "") + %name = %player.name; + if (%tag $= "") + { + %tag = getField(%player.tag, 0); + %append = getField(%player.tag, 1); + } + if (%append $= "") + %append = 0; + %recipient = %name TAB %tag TAB %append TAB %guid @ "\t"; + %recipients = %recipients @ %recipient; + } + %recipients = trim(%recipients); + Email_ToEdit.backing = %recipients; + Email_ToEdit.setText(tn_community_mailui_recipientShowList(Email_ToEdit.backing, 1)); + + //Email_ToEdit.setText(%MailTo); + Email_CCEdit.setText(""); + $EmailSubject = ""; + Canvas.pushDialog(EmailComposeDlg); + EmailBodyText.setValue(""); + Email_Subject.makeFirstResponder(1); +} + +// replacing function in webemail.cs, 369 +function CheckEmail(%schedule) +{ + if ($TribesNext::Community::UUID $= "") // session not established + return; + if (EmailGui.checkingEmail) + return; + + if (isEventPending(EmailGui.checkSchedule) && !%scheduled) + cancel(EmailGui.checkSchedule); + + EmailGui.checkSchedule = ""; + EMailGui.key = LaunchGui.key++; + EmailGui.state = "getMail"; + EmailGui.checkingEmail = true; + + // new code + if (EMailGui.initialDownloaded) + { + // initial messages have already been downloaded -- get updates + tn_community_mail_request_getNew("inbox"); + tn_community_mail_request_getNew("sentbox"); + tn_community_mail_request_getNew("deleted"); + } + else + { + // no initial messages downloaded yet -- get first chunk + // this will limit messages to the first chunk in each mailbox + // and further messages can be downloaded as the user scrolls to the bottom + EMailGui.initialDownloaded = 1; + tn_community_mail_request_boxList(0, $TribesNext::Community::Mail::ChunkSize, "inbox", 0); + tn_community_mail_request_boxList(0, $TribesNext::Community::Mail::ChunkSize, "sentbox", 0); + tn_community_mail_request_boxList(0, $TribesNext::Community::Mail::ChunkSize, "deleted", 0); + } + tn_community_mailui_clearCheckStatus(); +} + +// replacing function in webemail.cs, 1319 +function EM_Browser::onSelect(%this, %id) +{ + tn_community_mailui_loadSelected(); + + %text = EmailMessageVector.getLineTextByTag(%id); + if (rbinbox.getValue()) + { + if(!getRecord(%text, 2)) // read flag + { + %line = EmailMessageVector.getLineIndexByTag(%id); + %text = setRecord(%text, 2, 1); + + // Update the GUI: + %this.setRowFlags( %id, 1 ); + EmailMessageVector.deleteLine(%line); + EmailMessageVector.insertLine(%line, %text, %id); + } + } + EmailInboxBodyText.setValue(EmailGetTextDisplay(%text)); + EM_ReplyBtn.setActive( true ); + EM_ReplyToAllBtn.setActive( true ); + EM_ForwardBtn.setActive( true ); + EM_DeleteBtn.setActive( true ); + EM_BlockBtn.setActive( true ); +} + +// replacing function in webemail.cs, 297 +function EmailGetTextDisplay(%text) +{ + // get ID to check some additional properties + %id = getRecord(%text, 0); + %msg = tn_community_mail_getMessageObject(%id); + if (%msg.deleted $= "true") + %prepend = "Message has been deleted and will be removed from the mail system soon.\n"; + + %toList = getRecord(%text, 4); + %to = getLinkNameList(%toList); + %ccList = getRecord(%text, 5); + %ccLine = getLinkNameList(%ccList); + + %from = getLinkName(getRecord(%text, 1), 0); + %msgtext = "From: " @ %from NL + "To: " @ %to NL + "CC: " @ %ccLine NL + "Subject: " @ getRecord(%text, 6) NL + "Date Sent: " @ getRecord(%text, 3) @ "\n\n" @ + EmailGetBody(%text); + + return %prepend @ %msgtext; +} + +// replacing function in webemail.cs, 401 +function EmailEditBlocks() +{ + // this function is called when bringing up the block list editor + // -- initially this initiated a database query that would populate the UI + // as it was recieved. instead, this will be dealt with during the rest of the UI update + Canvas.pushDialog(EmailBlockDlg); + EmailBlockList.clear(); + EMailBlockDlg.key = LaunchGui.key++; + EmailBlockDlg.state = "getBlocklist"; + + EmailBlockDlg.gettingList = 1; + + tn_community_mail_request_viewList("ignore"); + tn_community_mailui_clearCheckStatus(); +} + +function tn_community_mailui_displayBlockList() +{ + // update the block list UI from the data interface cache + if ($TMail::ListMax["ignore"] $= "") + return; + %count = $TMail::ListMax["ignore"]; + for (%i = 0; %i <= %count; %i++) + { + %player = $TMail::ListVals["ignore", %i]; + + %name = getField(%player, 0); + %tag = getField(%player, 1); + %append = getField(%player, 2); + %guid = getField(%player, 3); + + %showName = (%append ? (%name @ %tag) : (%tag @ %name)); + EmailBlockList.addRow(%guid, %showName); + } +} + +// replacing function in webemail.cs, 410 +function EmailBlockSender() +{ + %id = EM_Browser.getSelectedId(); + if ( %id == -1 ) + { + MessageBoxOK("WARNING","You cannot block a non-existent sender."); + return; + } + else + { + %text = EmailMessageVector.getLineTextByTag(EM_Browser.getSelectedId()); + %blockUser = getRecord(%text, 1); + + %name = getField(%blockUser, 0); + %tag = getField(%blockUser, 1); + %append = getField(%blockUser, 2); + %guid = getField(%blockUser, 3); + + %showName = (%append ? (%name @ %tag) : (%tag @ %name)); + MessageBoxYesNo("CONFIRM BLOCK","Are you sure you want to block " @ %showName @ "?","tn_community_mail_request_addListEntry(\"ignore\", " @ %guid @ ");"); + } +} + +// replacing function in webemail.cs, 431 +function EmailBlockRemove() +{ + %rowId = EmailBlockList.getSelectedId(); + if(%rowId == -1) + { + MessageBoxOK("WARNING","You cannot remove a non-existent block."); + return; + } + else + { + %line = EmailBlockList.getRowTextById(%rowId); + %name = getField(%line, 2); + EMailBlockDlg.state = "removeBlock"; + EMailBlockDlg.key = LaunchGui.key++; + EmailBlockList.removeRowById(%rowId); + + tn_community_mail_request_delListEntry("ignore", %rowId); + } +} + +// replacing function in webemail.cs, 43 +function EmailMessageNew() +{ + Email_ToEdit.backing = ""; + Email_ToEdit.setText(""); + Email_CCEdit.backing = ""; + Email_CCEdit.setText(""); + $EmailSubject = ""; + EmailBodyText.setValue(""); + + EMailComposeDlg.state = "sendMail"; + Canvas.pushDialog(EmailComposeDlg); + Email_ToEdit.makeFirstResponder(1); +} + +// replacing function in webemail.cs, 55 +function EmailMessageReply() +{ + EMailComposeDlg.state = "replyMail"; + %text = EmailMessageVector.getLineTextByTag( EM_Browser.getSelectedId() ); + Email_ToEdit.backing = getRecord(%text, 1); + Email_ToEdit.setText(tn_community_mailui_recipientShowList(Email_ToEdit.backing, 1)); + Email_CCEdit.backing = ""; + Email_CCEdit.setText(""); + $EmailSubject = "RE: " @ getRecord(%text, 6); + %date = getRecord(%text, 3); + Canvas.pushDialog(EmailComposeDlg); + + %player = Email_ToEdit.getValue(); + %name = getField(%player, 0); + %tag = getField(%player, 1); + %append = getField(%player, 2); + %guid = getField(%player, 3); + + %showName = (%append ? (%name @ %tag) : (%tag @ %name)); + + EmailBodyText.setValue("\n\n----------------------------------\n On " @ %date SPC %showName @ " wrote:\n\n" @ EmailGetBody(%text) ); + EmailBodyText.SetCursorPosition(0); + EmailBodyText.makeFirstResponder(1); +} + +function EmailMessageForward() +{ + %text = EmailMessageVector.getLineTextByTag( EM_Browser.getSelectedId() ); + Email_ToEdit.backing = ""; + Email_ToEdit.setText(""); + Email_CCEdit.backing = ""; + Email_CCEdit.setText(""); + $EmailSubject = "FW: " @ getRecord(%text, 6); + Canvas.pushDialog(EmailComposeDlg); + EmailBodyText.setValue("\n\n\n--- Begin Forwarded Message ---\n\n" @ EmailGetTextDisplay(%text)); + Email_toEdit.makeFirstResponder(1); + EmailBodyText.SetCursorPosition(0); + EMailComposeDlg.state = "forwardMail"; +} +// replacing function in webemail.cs, 82 +function EmailMessageReplyAll() +{ + EMailComposeDlg.state = "replyAll"; + %text = EmailMessageVector.getLineTextByTag( EM_Browser.getSelectedId() ); + Email_ToEdit.backing = getRecord(%text, 1); + Email_ToEdit.setText(tn_community_mailui_recipientShowList(Email_ToEdit.backing, 1)); + Email_CCEdit.backing = getRecord(%text, 4) @ "\t" @ getRecord(%text,5); + Email_CCEdit.setText(tn_community_mailui_recipientShowList(Email_CCEdit.backing, 1)); + $EmailSubject = "RE: " @ getRecord(%text, 6); + %date = getRecord(%text, 3); + Canvas.pushDialog(EmailComposeDlg); + + %player = Email_ToEdit.getValue(); + %name = getField(%player, 0); + %tag = getField(%player, 1); + %append = getField(%player, 2); + %guid = getField(%player, 3); + + %showName = (%append ? (%name @ %tag) : (%tag @ %name)); + + EmailBodyText.setValue("\n\n===========================\n On " @ %date SPC %showName @ " wrote:\n\n" @ EmailGetBody(%text) ); + EmailBodyText.makeFirstResponder(1); + EmailBodyText.SetCursorPosition(0); +} + +// replacing function in webemail.cs, 145 +function EmailSend() +{ + EMailComposeDlg.key = LaunchGui.key++; + EMailComposeDlg.state = "sendMail"; + + %to = tn_community_mailui_extractRecipientIds(Email_ToEdit.backing); + %cc = tn_community_mailui_extractRecipientIds(Email_CCEdit.backing); + %subj = $EmailSubject; + %text = EMailBodyText.getValue(); + + tn_community_mail_request_send(%subj, %text, %to, %cc); + Canvas.popDialog(EmailComposeDlg); + + // run checkemail to update the list of messages in the mailboxes + CheckEmail(); +} + +function tn_community_mailui_extractRecipientIds(%recipients) +{ + %entries = getFieldCount(%recipients); + %guidList = ""; + for (%i = 0; %i < %entries; %i += 4) + { + %name = getField(%recipients, %i); + %tag = getField(%recipients, %i + 1); + %append = getField(%recipients, %i + 2); + %guid = getField(%recipients, %i + 3); + + %guidList = %guidList @ "," @ %guid; + } + return getSubStr(%guidList, 1, strlen(%guidList)); +} + +// replacing function in webemail.cs, 96 +function EmailMessageDelete() +{ + %id = EM_Browser.getSelectedId(); + if ( %id == -1 ) + return; + + EMailComposeDlg.key = LaunchGui.key++; + + // Make these buttons inactive until another message is selected: + %state = 1; + if ($TribesNext::Community::MailUI::ActiveMailbox $= "deleted") + %state = 0; + DoEmailDelete(%id, %state); +} + +// replacing function in webemail.cs, 121 +function DoEmailDelete(%mid, %state) +{ + %row = EM_Browser.findById(%mid); + + EM_ReplyBtn.setActive( false ); + EM_ReplyToAllBtn.setActive( false ); + EM_ForwardBtn.setActive( false ); + EM_DeleteBtn.setActive( false ); + EM_BlockBtn.setActive( false ); + + EM_Browser.removeRowByIndex(%row); + EmailMessageVector.deleteLine(EmailMessageVector.getLineIndexByTag(%mid)); + + if ( EM_Browser.rowCount() == 0 ) + EMailInboxBodyText.setText(""); + else + EM_Browser.setSelectedRow(%row); + + tn_community_mail_request_deleteMessage(%mid, %state); + tn_community_mailui_clearCheckStatus(); +} + +// replacing function in webemail.cs, 1017 +function EmailGui::ButtonClick(%this,%ord) +{ + switch(%ord) + { + case 0: // wired to inbox button + $TribesNext::Community::MailUI::ActiveMailbox = "inbox"; + tn_community_mailui_clearCheckStatus(); + case 1: // wired to deleted items button + $TribesNext::Community::MailUI::ActiveMailbox = "deleted"; + tn_community_mailui_clearCheckStatus(); + case 2: // newly wired to sent items button which was present, but hidden + $TribesNext::Community::MailUI::ActiveMailbox = "sentbox"; + tn_community_mailui_clearCheckStatus(); + } +} + +// replacing function in webemail.cs, 1229 +function EmailGui::loadCache(%this) { } +// replacing function in webemail.cs, 1274 +function EmailGui::dumpCache(%this) { } +// replacing function in webemail.cs, 1174 +function EmailGui::getCache(%this) { %this.cacheLoaded = true; } + +// addressdlg related functions in webemail + +// replacing function in webemail.cs, 823 +function AddressDlg::GoSearch(%this) +{ + if(trim(LC_Search.getValue()) !$="") + { + %this.key = LaunchGui.key++; + %this.state = "goSearch"; + %this.lbstate = "errorcheck"; + LC_BigList.mode = "select"; + + // searching via mail API + LC_BigList.clear(); + AddressDlg.searchActive = 1; + tn_community_mail_request_search(LC_Search.getValue()); + tn_community_mailui_clearCheckStatus(); + + LC_BuddyListBtn.direction = 0; + LC_BuddyListBtn.text = "ADD TO BUDDYLIST"; + LC_ListBox.setSelected(0); + } + else + MessageBoxOK("DENIED", "Blank searches for player names are not permitted. Please enter the start of the player name you are seeking."); +} + +function tn_community_mailui_displaySearchResults() +{ + if (LC_BigList.mode $= "select" || LC_BigList.mode $= "") + { + // update the search UI from the data interface cache + if ($TMail::SearchMax $= "") + return; + %count = $TMail::SearchMax; + for (%i = 0; %i <= %count; %i++) + { + %player = $TMail::SearchVals[%i]; + %guid = getField(%player, 3); + + if (%guid !$= "") + LC_BigList.addRow(%guid, %player); + } + } + else if (LC_BigList.mode $= "buddy") + { + // update the block list UI from the data interface cache + if ($TMail::ListMax["buddy"] $= "") + return; + %count = $TMail::ListMax["buddy"]; + for (%i = 0; %i <= %count; %i++) + { + %player = $TMail::ListVals["buddy", %i]; + %guid = getField(%player, 3); + if (%guid !$= "") + LC_BigList.addRow(%guid, %player); + } + } +} + +// replacing function in webemail.cs, 742 +function AddressDlg::AddBuddylist(%this) +{ + %this.key = LaunchGui.key++; + %this.lbstate = "buddylist"; + + switch (%this.SrcList) + { + case 0: + %addremove = LC_BuddyListBtn.direction; + %player = LC_BigList.getValue(); + %selRow = LC_BigList.getRownumByID(LC_BigList.GetSelectedID()); + case 1: + %addremove = 0; + %player = LC_ToList.getValue(); + case 2: + %addremove = 0; + %player = LC_CCList.getValue(); + } + %guid = getField(%player, 3); + + if (%guid !$= "") + { + if (%addremove==0) + { + %this.doRefresh = 1; + %this.state = "addBuddy"; + tn_community_mail_request_addListEntry("buddy", %guid); + //DatabaseQuery(10,%player,%this,%this.key); + } + else + { + %this.state = "dropBuddy"; + tn_community_mail_request_delListEntry("buddy", %guid); + //DatabaseQuery(11,%player,%this,%this.key); + LC_BigList.removeRowbyId(LC_BigList.getSelectedID()); + if(%selRow>=LC_BigList.RowCount()) + %selRow = LC_BigList.RowCount()-1; + LC_BigList.setSelectedRow(%selRow); + } + } + else + { + error("Trying to modify buddy list on player data missing GUID: " @ %player); + } +} + +// replacing function in webemail.cs, 839 +function AddressDlg::GoList(%this) +{ + %this.key = LaunchGui.key++; + %this.lbstate = "errorcheck"; + if(LC_ListBox.getValue() $= "Select List") + { + LC_BigList.mode = "select"; + LC_BigList.clear(); + } + else if(LC_ListBox.getValue() $= "Buddy List") + { + LC_BigList.mode = "buddy"; + LC_BigList.clear(); + %this.state = "getBuddyList"; + AddressDlg.searchActive = 1; + tn_community_mail_request_viewList("buddy"); + LC_BuddyListBtn.direction = 1; + LC_BuddyListBtn.text = "REMOVE FROM BUDDYLIST"; + } + else + { + LC_BigList.mode = "clan"; + LC_BigList.clear(); + + LC_BuddyListBtn.direction = 0; + LC_BuddyListBtn.text = "ADD TO BUDDYLIST"; + + %clanid = LC_ListBox.getSelected(); + %clan = tn_community_browser_getClanProfile(%clanid); + + %pcount = %clan.pcount; + if (%pcount !$= "") + { + for (%i = 0; %i <= %pcount; %i++) + { + %member = %clan.player[%i]; + %mguid = getField(%member, 3); + if (%mguid != getField(WONGetAuthInfo(), 3)) + LC_BigList.addRow(%mguid, getFields(%member, 0, 3)); + } + } + } + tn_community_mailui_clearCheckStatus(); +} + +// replacing function in webemail.cs, 777 +function AddressDlg::AddCC(%this) +{ + if(LC_CCListBtn.direction == 0) + { + %addName = LC_BigList.getRowTextById(LC_BigList.getSelectedID()); + %hasDupes = CheckAllDuplicates(%addName); + if(%hasDupes == 0) + LC_CCList.addRow(LC_CCList.RowCount()+1, %addName); + } + else + { + %selRow = LC_CCList.getRownumByID(LC_CCList.GetSelectedID()); + LC_CCList.removeRowbyID(LC_CCList.getSelectedID()); + if(%selRow>=LC_CCList.RowCount()) + %selRow = LC_CCList.RowCount()-1; + LC_CCList.setSelectedRow(%selRow); + } + %this.DestList = 1; +} + +// replacing function in webemail.cs, 797 +function AddressDlg::AddTo(%this) +{ + if(LC_ToListBtn.direction == 0) + { + %addName = LC_BigList.getRowTextById(LC_BigList.getSelectedID()); + %hasDupes = CheckAllDuplicates(%addName); + if(%hasDupes == 0 ) + LC_ToList.addRow(LC_ToList.RowCount()+1, %addName); + } + else + { + %selRow = LC_ToList.getRownumByID(LC_ToList.GetSelectedID()); + LC_ToList.removeRowbyID(LC_ToList.getSelectedID()); + if(%selRow>=LC_ToList.RowCount()) + %selRow = LC_ToList.RowCount()-1; + LC_ToList.SetSelectedRow(%selRow); + } + %this.DestList = 0; +} + +// replacing function in webemail.cs, 863 +function AddressDlg::OK(%this) +{ + EMail_ToEdit.backing = ListToStr(LC_ToList,"\t"); + EMail_CCEdit.backing = ListToStr(LC_CCList,"\t"); + + tn_community_mailui_recipientValidate(); + + LC_BigList.Clear(); + Canvas.PopDialog("AddressDlg"); +} + +// replacing function in webemail.cs, 602 +function ListToStr(%listName,%delim) +{ + %str = ""; + %rCount = %listName.rowCount(); + if (%rCount > 0) + { + for(%r=0;%r<%rCount;%r++) + { + %str = %str @ %listName.getRowText(%r); + if(%r < %rCount-1) + { + %str = %str @ %delim; + } + } + } + return %str; +} + +function tn_community_mailui_populateRList(%list, %backing) +{ + %entries = getFieldCount(%backing); + %showList = ""; + for (%i = 0; %i < %entries; %i += 4) + { + %name = getField(%backing, %i); + %tag = getField(%backing, %i + 1); + %append = getField(%backing, %i + 2); + %guid = getField(%backing, %i + 3); + + %list.addRow(%guid, %name TAB %tag TAB %append TAB %guid); + } +} + +// replacing function in webemail.cs, 953 +function AddressDlg::onWake(%this) +{ + %this.doRefresh = 0; + %this.key = LaunchGui.key++; + %this.state = "loadlistbox"; + %this.lbstate = "errorcheck"; + %this.DestList = 0; + %this.SrcList = 0; + LC_BuddyListBtn.setVisible(0); + LC_ListBox.Clear(); + LC_ListBox.Add("Select List",0); + LC_ListBox.Add("Buddy List",1); + LC_ListBox.setSelected(0); + LC_Search.clear(); + + //StrToList(LC_ToList,Email_ToEdit.getValue(),","); + LC_ToList.clear(); + tn_community_mailui_populateRList(LC_ToList, EMail_ToEdit.backing); + //StrToList(LC_CCList,Email_CCEdit.getValue(),","); + LC_CCList.clear(); + tn_community_mailui_populateRList(LC_CcList, EMail_CCEdit.backing); + + // retrieve a list of clans the user is in + // and make the list of members available for easy emailing. + %myguid = getField(WONGetAuthInfo(), 3); + %player = tn_community_browser_getPlayerProfile(%myguid); + + %mcount = %player.mcount; + if (%mcount !$= "") + { + %ptag = getField(%player.tag, 0); + for (%i = 0; %i <= %mcount; %i++) + { + %membership = %player.membership[%i]; + %mid = getField(%membership, 0); + %mname = getField(%membership, 1); + + LC_ListBox.add(%mname, %mid); + } + } +} + +package tn_tmail +{ + function EmailGui::onWake(%this) + { + Parent::onWake(%this); + + // Make these buttons inactive until a message is selected: + EM_ReplyBtn.setActive( false ); + EM_ReplyToAllBtn.setActive( false ); + EM_ForwardBtn.setActive( false ); + EM_DeleteBtn.setActive( false ); + EM_BlockBtn.setActive( false ); + %selId = EM_Browser.getSelectedId(); + Canvas.pushDialog(LaunchToolbarDlg); + + if ( EM_Browser.rowCount() > 0 ) + { + %row = EM_Browser.findById( %selId ); + if ( %row == -1 ) + EM_Browser.setSelectedRow( 0 ); + else + EM_Browser.setSelectedRow( %row ); + } + + //error("EmailGui::onWake: " @ %this); + $TribesNext::Community::MailUI::Awake = 1; + tn_community_mailui_loadSelected(); + } + + function EmailGui::onSleep(%this) + { + Parent::onSleep(%this); + $TribesNext::Community::MailUI::Awake = 0; + //error("EmailGui::onSleep: " @ %this); + } +}; +if (!isActivePackage(tn_tmail)) + activatePackage(tn_tmail); \ No newline at end of file diff --git a/t2csri/community/settings.cs b/t2csri/community/settings.cs new file mode 100644 index 0000000..0a1abbe --- /dev/null +++ b/t2csri/community/settings.cs @@ -0,0 +1,18 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2011 + +// Tribes 2 Community System +// Robot Client Settings + +// This file contains the URL and server settings for the community system. + +$TribesNext::Community::Host = "localhost"; +$TribesNext::Community::Port = 80; +$TribesNext::Community::BaseURL = "/tn/robot/"; + +$TribesNext::Community::LoginScript = "robot_login.php"; +$TribesNext::Community::MailScript = "robot_mail.php"; +$TribesNext::Community::BrowserScript = "robot_browser.php"; + +$TribesNext::Community::SessionRefresh = 10*60; \ No newline at end of file diff --git a/t2csri/crypto.rb b/t2csri/crypto.rb new file mode 100644 index 0000000..4c5ed26 --- /dev/null +++ b/t2csri/crypto.rb @@ -0,0 +1,492 @@ +# +# Tribes 2 Community System Reengineering Initiative +# Assymetric Cryptography Identity Provisioning +# Version 1.0 +# +# Written by Electricutioner/Thyth +# http://absolous.no-ip.com/ +# Copyright 2008 +# +# Released under the terms of the GNU General Public License v3 or later. +# http://www.gnu.org/licenses/gpl.html +# Your use of this software is subject to the terms of that license. Use, modification, or distribution +# constitutes acceptance of these software terms. This license is the only manner by which you are permitted +# to use this software, thus rejection of the license terms prohibits your use of this software. +# + +# fast modular exponentiation -- the key to the RSA algorithm +# result = (b ^ e) % m +def rsa_mod_exp(b, e, m) + result = 1 + while (e > 0) + if ((e & 1) == 1) + result = (result * b) % m + end + e = e >> 1 + b = (b * b) % m + end + return result +end + +# RSA key class to keep things nice and organized +class RSAKey + # allow reading and writing the key values + attr_reader :e, :n, :twister, :strength + attr_writer :e, :d, :n, :twister + + # allow protecting the d value so it isn't stolen by evil scripts + # once a key is protected, it cannot be deprotected, but it can be used to decrypt + def protect + @protected = 1 + end + # attribute reader for d that returns nil if key protection is active + def d + if (@protected == 1) + return nil + else + return @d + end + end + + # encrypt a message with the public exponent (e) + # this could be construed as a misnomer, since this is used to verify authentication + # images from the authentication server, and to verify a client has both parts of the key they + # claim to have + def encrypt(message) + rsa_mod_exp(message, @e, @n) + end + + # decrypt a message with the private exponent (d), also usable for signing + # obviously, this will fail if the instance is only the public part of the key + def decrypt(message) + rsa_mod_exp(message, @d, @n) + end + + # generate a new random RSA key of the specified bitsize + # this generates keys that should be resistant to quick factorization techniques + def generate(bitsize) + p = 0 + q = 0 + @n = 100 + @strength = bitsize + + # test for some conditions that could produce insecure RSA keys + # p, q difference to see if Fermat factorization could be successful + # p - q must be greater than 2*(n ^ (1/4)) + while ((p - q).abs < (2 * Math.sqrt(Math.sqrt(@n)))) + p = createPrime(bitsize / 2, 150) + q = createPrime(bitsize / 2, 150) + @n = p * q + end + + totient = (p - 1) * (q - 1) + + # e must be coprime to the totient. we start at 3 and add 2 whenever coprime test fails + @e = 3 + coprimee = 0 + while (coprimee) + if (@e > 7) + # e over 7 has a large chance of not being coprime to the totient + generate(bitsize) + return + end + block = extendedEuclid(@e, totient, 0, 1, 1, 0) + if (block[0] > 1) + @e = @e + 2 + else + coprimee = nil + end + end + + # calculate the d value such that d * e = 1 mod totient + # this calculation is done in the coprime of e verification + @d = block[1] + while (@d < 0) + @d = @d + totient + end + + # verify that the generated key is a valid RSA key + 1.upto(10) do |i| + testVal = @twister.randomnumber(bitsize) % @n + if (decrypt(encrypt(testVal)) != testVal) + # key failed... generate a new one + generate(bitsize) + return + end + end + end + + # private methods that people shouldn't be poking without a good reason + private + # obtain gcd and return the "d" value that we want + def extendedEuclid(a, b, c, d, e, f) + if (b == 0) + block = Array.new(3, 0) + block[0] = a; # gcd(a, b) + block[1] = e; # coefficient of 'a' and the 'd' value we want + block[2] = f; # coefficient of 'b' + return block + else + return extendedEuclid(b, a % b, e - ((a / b) * c), f - ((a / b) * d), c, d); + end + end + + # create a prime number of the specified bitlength + # the number of tests specified will control how many miller-rabin primality tests are run + # this function will return a prime number with a high degree of confidence if sufficient + # tests are run + def createPrime(bitlen, tests) + # generate a random number of the specific bitlen + p = @twister.randomnumber(bitlen) + + # run the primality tests + testrun = 0 + while (testrun < tests) + if (prime?(p)) + testrun = testrun + 1 + else # not prime -- generate a new one + return createPrime(bitlen, tests) + end + end + return p + end + + # run a miller-rabin primality test on the given number + # returns true if the number is "probably" prime + def prime?(potential) + qandm = getqm(potential) + if (qandm[0] == -1) + return nil + end + + bval = @twister.randomnumber(@strength / 2) + mval = qandm[1] + + if (rsa_mod_exp(bval, mval, potential) == 1) + return 1 + end + j = 0 + while (j < qandm[0]) + if ((potential - 1) == rsa_mod_exp(bval, mval, potential)) + return 1 + end + mval = mval * 2 + j = j + 1 + end + return nil + end + + def getqm(p) + p = p - 1 + rt = Array.new(2, 0) + if (p & 1 != 0) + rt[0] = -1 + rt[1] = -1 + return rt + end + div = p / 2 + counter = 1 + while (div & 1 == 0) + counter = counter + 1 + div = div / 2 + end + rt[0] = counter + rt[1] = div + return rt + end +end + +# Mersenne Twister pseudo random number generator, modified for cryptographic security +# period length should be 20 * (2 ^ 19937 - 1) +class MersenneTwister + @index = 0 + + # build the internal storage array + def initialize + @mt = Array.new(624, 0) + end + + # initialize the generator from a seed, can be done repeatedly + def seedgen(seed) + @mt[0] = seed + 1.upto(623) do |i| + @mt[i] = 0xffffffff & (1812433243 * (@mt[i - 1] ^ (@mt[i - 1] >> 30)) + i) + end + generateNumbers + end + + # extract a number that does not give away the state of the generator, takes 37 elements from generator + # and applies SHA1 on it to get a 20 element number. this is repeated until the required length + # is reached, and truncated as necessary to bring it down to the requested bitlen + def randomnumber(bits) + bytes = bits / 8 + if (bits % 8 != 0) + bytes = bytes + 1 + end + + produced = 0 + output = 0 + stages = 0 + mask = 0 + + sha1hash = SHA1Pure.new + while (produced < bytes) + sha1hash.prepare + 1.upto(37) do |i| + sha1hash.append(extractNumber().to_s); + end + digest = sha1hash.hexdigest.to_i(16) + output = output | (digest << (160 * stages)) + produced = produced + 20 + stages = stages + 1 + end + + 0.upto(bits.to_i) do |i| + mask = (mask.to_i << 1) | 1 + end + return (output & mask) + end + + private + # extract a tempered pseudorandom number + def extractNumber() + if (@index == 0) + generateNumbers() + end + + y = @mt[@index.to_i] + y = y ^ (y >> 11) + y = y ^ ((y << 7) & 2636928640) + y = y ^ ((y << 15) & 4022730752) + y = y ^ (y >> 18) + y = y & 0xffffffff + + @index = (@index.to_i + 1) % 624 + return y + end + + # generate 624 untempered numbers for this generator's array + def generateNumbers() + 0.upto(623) do |i| + y = (@mt[i] & 0x80000000) + (@mt[(i + 1) % 624] & 0x7FFFFFFF) + @mt[i] = @mt[(i + 397) % 624] ^ (y >> 1) + if (y & 1 == 1) + @mt[i] = @mt[i] ^ 2567483615 + end + end + end +end + +# SHA1 in Pure Ruby +class SHA1Pure + + def initialize + prepare + end + + # prepare the hash digester for a new hash + def prepare + @state = Array.new(5, 0) + @block = Array.new(16, 0) + @blockIndex = 0 + @count = 0 + + @state[0] = 0x67452301 + @state[1] = 0xefcdab89 + @state[2] = 0x98badcfe + @state[3] = 0x10325476 + @state[4] = 0xc3d2e1f0 + end + + # append a string to the string being digested + def append(str) + str = str.to_s + str.each_byte {|c| update(c.to_i & 0xff)} + end + + # produce a hexidecimal digest string + def hexdigest + bits = Array.new(8, 0) + 0.upto(7) do |i| + bits[i] = (@count >> (((7 - i) * 8) & 0xff)) & 0xff + end + update(128) + while (@blockIndex != 56) + update(0) + end + 0.upto(7) do |i| + update(bits[i]) + end # this will accomplish a transform + + # output the digest + digest = "" + 0.upto(4) do |i| + chunk = @state[i].to_s(16) + while(chunk.length < 8) + chunk = "0" + chunk + end + digest = digest + chunk + end + prepare + return digest + end + + private + def rol(val, bits) + val = val.to_i + bits = bits.to_i + return (val << bits) | (val >> (32 - bits)) + end + + def blk0(i) + i = i.to_i + @block[i] = (rol(@block[i], 24) & 0xff00ff00) | (rol(@block[i], 8) & 0xff00ff) + @block[i] = @block[i] & 0xffffffff + return @block[i] + end + + def blk(i) + i = i.to_i + @block[i & 15] = rol(@block[(i + 13) & 15] ^ @block[(i + 8) & 15] ^ @block[(i + 2) & 15] ^ @block[i & 15], 1) + @block[i & 15] = @block[i & 15] & 0xffffffff + return @block[i & 15] + end + + def r0(data, v, w, x, y, z, i) + data[z] += ((data[w] & (data[x] ^ data[y])) ^ data[y]) + blk0(i) + 0x5a827999 + rol(data[v], 5) + data[z] = data[z] & 0xffffffff + data[w] = rol(data[w], 30) & 0xffffffff + end + + def r1(data, v, w, x, y, z, i) + data[z] += ((data[w] & (data[x] ^ data[y])) ^ data[y]) + blk(i) + 0x5a827999 + rol(data[v], 5) + data[z] = data[z] & 0xffffffff + data[w] = rol(data[w], 30) & 0xffffffff + end + + def r2(data, v, w, x, y, z, i) + data[z] += (data[w] ^ data[x] ^ data[y]) + blk(i) + 0x6ed9eba1 + rol(data[v], 5) + data[z] = data[z] & 0xffffffff + data[w] = rol(data[w], 30) & 0xffffffff + end + + def r3(data, v, w, x, y, z, i) + data[z] += (((data[w] | data[x]) & data[y]) | (data[w] & data[x])) + blk(i) + 0x8f1bbcdc + rol(data[v], 5) + data[z] = data[z] & 0xffffffff + data[w] = rol(data[w], 30) & 0xffffffff + end + + def r4(data, v, w, x, y, z, i) + data[z] += (data[w] ^ data[x] ^ data[y]) + blk(i) + 0xca62c1d6 + rol(data[v], 5) + data[z] = data[z] & 0xffffffff + data[w] = rol(data[w], 30) & 0xffffffff + end + + def transform + dd = Array.new(5, 0) + dd[0] = @state[0] + dd[1] = @state[1] + dd[2] = @state[2] + dd[3] = @state[3] + dd[4] = @state[4] + + r0(dd,0,1,2,3,4, 0) + r0(dd,4,0,1,2,3, 1) + r0(dd,3,4,0,1,2, 2) + r0(dd,2,3,4,0,1, 3) + r0(dd,1,2,3,4,0, 4) + r0(dd,0,1,2,3,4, 5) + r0(dd,4,0,1,2,3, 6) + r0(dd,3,4,0,1,2, 7) + r0(dd,2,3,4,0,1, 8) + r0(dd,1,2,3,4,0, 9) + r0(dd,0,1,2,3,4,10) + r0(dd,4,0,1,2,3,11) + r0(dd,3,4,0,1,2,12) + r0(dd,2,3,4,0,1,13) + r0(dd,1,2,3,4,0,14) + r0(dd,0,1,2,3,4,15) + r1(dd,4,0,1,2,3,16) + r1(dd,3,4,0,1,2,17) + r1(dd,2,3,4,0,1,18) + r1(dd,1,2,3,4,0,19) + r2(dd,0,1,2,3,4,20) + r2(dd,4,0,1,2,3,21) + r2(dd,3,4,0,1,2,22) + r2(dd,2,3,4,0,1,23) + r2(dd,1,2,3,4,0,24) + r2(dd,0,1,2,3,4,25) + r2(dd,4,0,1,2,3,26) + r2(dd,3,4,0,1,2,27) + r2(dd,2,3,4,0,1,28) + r2(dd,1,2,3,4,0,29) + r2(dd,0,1,2,3,4,30) + r2(dd,4,0,1,2,3,31) + r2(dd,3,4,0,1,2,32) + r2(dd,2,3,4,0,1,33) + r2(dd,1,2,3,4,0,34) + r2(dd,0,1,2,3,4,35) + r2(dd,4,0,1,2,3,36) + r2(dd,3,4,0,1,2,37) + r2(dd,2,3,4,0,1,38) + r2(dd,1,2,3,4,0,39) + r3(dd,0,1,2,3,4,40) + r3(dd,4,0,1,2,3,41) + r3(dd,3,4,0,1,2,42) + r3(dd,2,3,4,0,1,43) + r3(dd,1,2,3,4,0,44) + r3(dd,0,1,2,3,4,45) + r3(dd,4,0,1,2,3,46) + r3(dd,3,4,0,1,2,47) + r3(dd,2,3,4,0,1,48) + r3(dd,1,2,3,4,0,49) + r3(dd,0,1,2,3,4,50) + r3(dd,4,0,1,2,3,51) + r3(dd,3,4,0,1,2,52) + r3(dd,2,3,4,0,1,53) + r3(dd,1,2,3,4,0,54) + r3(dd,0,1,2,3,4,55) + r3(dd,4,0,1,2,3,56) + r3(dd,3,4,0,1,2,57) + r3(dd,2,3,4,0,1,58) + r3(dd,1,2,3,4,0,59) + r4(dd,0,1,2,3,4,60) + r4(dd,4,0,1,2,3,61) + r4(dd,3,4,0,1,2,62) + r4(dd,2,3,4,0,1,63) + r4(dd,1,2,3,4,0,64) + r4(dd,0,1,2,3,4,65) + r4(dd,4,0,1,2,3,66) + r4(dd,3,4,0,1,2,67) + r4(dd,2,3,4,0,1,68) + r4(dd,1,2,3,4,0,69) + r4(dd,0,1,2,3,4,70) + r4(dd,4,0,1,2,3,71) + r4(dd,3,4,0,1,2,72) + r4(dd,2,3,4,0,1,73) + r4(dd,1,2,3,4,0,74) + r4(dd,0,1,2,3,4,75) + r4(dd,4,0,1,2,3,76) + r4(dd,3,4,0,1,2,77) + r4(dd,2,3,4,0,1,78) + r4(dd,1,2,3,4,0,79) + + @state[0] = (@state[0] + dd[0]) & 0xffffffff + @state[1] = (@state[1] + dd[1]) & 0xffffffff + @state[2] = (@state[2] + dd[2]) & 0xffffffff + @state[3] = (@state[3] + dd[3]) & 0xffffffff + @state[4] = (@state[4] + dd[4]) & 0xffffffff + end + + def update(b) + mask = (8 * (@blockIndex & 3)) + @count = @count + 8 + @block[@blockIndex >> 2] = @block[@blockIndex >> 2] & ~(0xff << mask) + @block[@blockIndex >> 2] = @block[@blockIndex >> 2] | ((b & 0xff) << mask) + @blockIndex = @blockIndex + 1 + if (@blockIndex == 64) + transform + @blockIndex = 0 + end + end +end \ No newline at end of file diff --git a/t2csri/glue.cs b/t2csri/glue.cs new file mode 100644 index 0000000..2560c36 --- /dev/null +++ b/t2csri/glue.cs @@ -0,0 +1,47 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Thyth +// Copyright 2008-2011 by Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 1.2 initialization and glue file + +// enable debugging console +//enableWinConsole(1); + +// check to see if the game has been launched in offline mode +function t2csri_glue_initChecks() +{ + $t2csri::isOfflineMode = 0; + for (%i = 0; %i < $Game::argc; %i++) + { + %arg = $Game::argv[%i]; + if (%arg $= "-nologin") + $t2csri::isOfflineMode = 1; + } + if ($t2csri::isOfflineMode) + { + echo("Running TribesNext in offline mode. Not making connections to the Internet."); + } +} +t2csri_glue_initChecks(); + +// load the torque script components +exec("t2csri/authconnect.cs"); +exec("t2csri/authinterface.cs"); +exec("t2csri/base64.cs"); +exec("t2csri/clientSide.cs"); +exec("t2csri/ipv4.cs"); +exec("t2csri/rubyUtils.cs"); + +// load the Ruby components +rubyExec("t2csri/crypto.rb"); +rubyExec("t2csri/certstore.rb"); + +rubyEval("certstore_loadAccounts"); +rubyEval("tsEval '$RubyEnabled=1;'"); + +// connect to the auth server via signed lookup +schedule(32, 0, authConnect_findAuthServer); + +// get the global IP for sanity testing purposes +schedule(32, 0, ipv4_getInetAddress); diff --git a/t2csri/ipv4.cs b/t2csri/ipv4.cs new file mode 100644 index 0000000..184c47f --- /dev/null +++ b/t2csri/ipv4.cs @@ -0,0 +1,106 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// IPv4 Utils Version 1.1 (03/26/2008) + +// Whatismyip spat this out for automation purposes: +// http://www.whatismyip.com/automation/n09230945.asp +// Hopefully it won't change. We only check for extern-ip once +// when the game launches, so there shouldn't be more than a +// couple of hundred hits per day from the entire T2 community. + +$IPv4::AutomationURL = "/whatismyip.php"; + +function ipv4_getInetAddress() +{ + if ($IPv4::InetAddress !$= "") + return; + + if (isObject(IPv4Connection)) + { + IPv4Connection.disconnect(); + IPv4Connection.delete(); + } + new TCPObject(IPv4Connection); + IPV4Connection.data = "GET " @ $IPv4::AutomationURL @ " HTTP/1.1\r\nHost: www.tribesnext.com\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n"; + IPv4Connection.connect("www.tribesnext.com:80"); +} + +function IPv4Connection::onConnected(%this) +{ + %this.send(%this.data); +} + +function IPv4Connection::onLine(%this, %line) +{ + if (%line $= "" || %line == 0) + return; + $IPv4::InetAddress = %line; + %this.disconnect(); +} + +// added for 1.1, schedule a new attempt if we're blank, until we have an address +function IPv4Connection::onDisconnect(%this) +{ + schedule(5000, 0, ipv4_getInetAddress); +} + +// used for the IP-nonce sanity check... +// source will claim that this computer is the destination. +// check to make sure the destination is reasonable +function ipv4_reasonableConnection(%source, %destination) +{ + if (%destination $= $IPv4::InetAddress) + { + // the destination claims to be us from the Internet. This is reasonable. + return 1; + } + else + { + // destination is different from the IPv4 Internet Address. We could be on a LAN. + if (getSubStr(%destination, 0, 2) $= "10") + { + // Class A LAN, check if the client is also on the same network + return (getSubStr(%source, 0, 2) $= "10"); + } + else if (getSubStr(%destination, 0, 3) $= "172" && getSubStr(%destination, 4, 2) > 15 && getSubStr(%destination, 4, 2) < 33) + { + // Class B LAN, check if the client is also on the same network + return (getSubStr(%source, 0, 3) $= "172" && getSubStr(%source, 4, 2) > 15 && getSubStr(%source, 4, 2) < 33); + } + else if (getSubStr(%destination, 0, 7) $= "192.168") + { + // Class C LAN, check if the client is also on the same network + return (getSubStr(%source, 0, 7) $= "192.168"); + } + else if (getSubStr(%destination, 0, 7) $= "169.254") + { + // Link-local addresses/Zeroconf network, check if client is from the same place + return (getSubStr(%source, 0, 7) $= "169.254"); + } + else if (%destination $= $Host::BindAddress) + { + // Or it could be the pref-based bind address. + return 1; + } + else + { + // looks like the destination address provided by the source is not reasonable + // this is likely an attempt at a client token replay attack + return 0; + } + } +} + + +// convert a (big endian) hex block into a numeric IP +function ipv4_hexBlockToIP(%hex) +{ + for (%i = 0; %i < 4; %i++) + { + %ip = %ip @ "." @ strcmp(collapseEscape("\\x" @ getSubStr(%hex, %i * 2, 2)), ""); + } + return getSubStr(%ip, 1, strlen(%ip) - 1); +} diff --git a/t2csri/postLogin.cs b/t2csri/postLogin.cs new file mode 100644 index 0000000..5431e15 --- /dev/null +++ b/t2csri/postLogin.cs @@ -0,0 +1,19 @@ +// TribesNext Project +// http://www.tribesnext.com/ +// Copyright 2011 + +// Tasks to be run after the login process is completed. + +// load the community script components +if (WONGetAuthInfo() !$= "") +{ + exec("t2csri/community/settings.cs"); + exec("t2csri/community/login.cs"); + exec("t2csri/community/mail.cs"); + exec("t2csri/community/browser.cs"); + schedule(32, 0, exec, "t2csri/community/mailUI.cs"); + schedule(64, 0, exec, "t2csri/community/browserUI.cs"); + + // log into the community server + tn_community_login_initiate(); +} \ No newline at end of file diff --git a/t2csri/rubyUtils.cs b/t2csri/rubyUtils.cs new file mode 100644 index 0000000..60f9a48 --- /dev/null +++ b/t2csri/rubyUtils.cs @@ -0,0 +1,31 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008-2009 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Ruby Interface Utilities Version 1.3 (01/27/2009) + +// loads a ruby script +function rubyExec(%script) +{ + echo("Loading Ruby script " @ %script @ "."); + new FileObject("RubyExecutor"); + RubyExecutor.openForRead(%script); + + while (!RubyExecutor.isEOF()) + { + %line = RubyExecutor.readLine(); + %buffer = %buffer @ "\n" @ %line; + } + rubyEval(%buffer); + RubyExecutor.close(); + RubyExecutor.delete(); +} + +// extracts a value from the Ruby interpreter environment +function rubyGetValue(%value) +{ + $temp = ""; + rubyEval("tsEval '$temp=\"' + " @ %value @ " + '\";'"); + return $temp; +} diff --git a/t2csri/serverSide.cs b/t2csri/serverSide.cs new file mode 100644 index 0000000..e200dff --- /dev/null +++ b/t2csri/serverSide.cs @@ -0,0 +1,322 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 1.3: 2009-04-23 +// Clan/Rename Certificate support is included in this version. + +// initialize the SHA1 digester in Ruby +function t2csri_initDigester() +{ + $SHA1::Initialized = 1; + rubyEval("$sha1hasher = SHA1Pure.new"); +} + +// use Ruby to get the SHA1 hash of the string +function sha1sum(%string) +{ + if (!$SHA1::Initialized) + t2csri_initDigester(); + %string = strReplace(%string, "'", "\\'"); + rubyEval("$sha1hasher.prepare"); + rubyEval("$sha1hasher.append('" @ %string @ "')"); + rubyEval("tsEval '$temp=\"' + $sha1hasher.hexdigest + '\";'"); + %temp = $temp; + $temp = ""; + return %temp; +} + +// verify with the auth server's RSA public key... hard coded in the executable +function t2csri_verify_auth_signature(%sig) +{ + rubyEval("tsEval '$temp=\"' + t2csri_verify_auth_signature('" @ %sig @ "').to_s(16) + '\";'"); + while (strLen($temp) < 40) + $temp = "0" @ $temp; + return $temp; +} + +// server sends the client a certificate in chunks, since they can be rather large +function serverCmdt2csri_sendCertChunk(%client, %chunk) +{ + if (%client.doneAuthenticating) + return; + + //echo("Client sent certificate chunk."); + %client.t2csri_cert = %client.t2csri_cert @ %chunk; + if (strlen(%client.t2csri_cert) > 20000) + { + %client.setDisconnectReason("Account certificate too long. Check your account key for corruption."); + %client.delete(); + } +} + +// gets a hex version of the client's IP address +// used to prevent a replay attack as described by Rain +function t2csri_gameClientHexAddress(%client) +{ + %ip = %client.getAddress(); + %ip = getSubStr(%ip, strstr(%ip, ":") + 1, strlen(%ip)); + %ip = getSubStr(%ip, 0, strstr(%ip, ":")); + %ip = strReplace(%ip, ".", " "); + + for (%i = 0; %i < getWordCount(%ip); %i++) + { + %byte = DecToHex(getWord(%ip, %i)); + if (strLen(%byte) < 2) + %byte = "0" @ %byte; + %hex = %hex @ %byte; + } + return %hex; +} + +// client is done sending their cert... verify it, and encrypt a challenge for the client +// challenge sent to client is %clientChallenge @ %serverChallenge. +function serverCmdt2csri_sendChallenge(%client, %clientChallenge) +{ + if (%client.doneAuthenticating) + return; + + //echo("Client requesting challenge. CC: " @ %clientChallenge); + //echo("Client's certificate: " @ %client.t2csri_cert); + // verify that the certificate the client sent is signed by the authentication server + %user = strReplace(getField(%client.t2csri_cert, 0), "\x27", "\\\x27"); + + %guid = getField(%client.t2csri_cert, 1); + // sanitize GUID + for (%i = 0; %i < strlen(%guid); %i++) + { + %char = strcmp(getSubStr(%guid, %i, 1), ""); + if (%char > 57 || %char < 48) + { + %client.setDisconnectReason("Invalid characters in client GUID."); + %client.delete(); + return; + } + } + + %e = getField(%client.t2csri_cert, 2); + %n = getField(%client.t2csri_cert, 3); + %sig = getField(%client.t2csri_cert, 4); + + // sanitize e, n, sig... all of which are just hex + %rsa_chunk = strlwr(%e @ %n @ %sig); + for (%i = 0; %i < strlen(%rsa_chunk); %i++) + { + %char = strcmp(getSubStr(%rsa_chunk, %i, 1), ""); + if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97)) + { + %client.setDisconnectReason("Invalid characters in certificate RSA fields."); + %client.delete(); + return; + } + } + + // get a SHA1 sum + %sumStr = %user @ "\t" @ %guid @ "\t" @ %e @ "\t" @ %n; + %certSum = sha1sum(%sumStr); + %verifSum = t2csri_verify_auth_signature(%sig); + while (strLen(%verifSum) < 40) + %verifSum = "0" @ %verifSum; + //echo("Calc'd SHA1: " @ %certSum); + //echo("Signed SHA1: " @ %verifSum); + + // verify signature + if (%verifSum !$= %certSum) + { + // client supplied a bogus certificate that was never signed by the auth server + // abort their connection + %client.setDisconnectReason("Invalid account certificate."); + %client.delete(); + return; + } + + // process client challenge half + %client.t2csri_clientChallenge = %clientChallenge; + + // sanitize the challenge to make sure it contains nothing but hex characters. + // anything else means that the client is trying to hijack control of the interpreter + %clientChallenge = strlwr(%clientChallenge); + for (%i = 0; %i < strlen(%clientChallenge); %i++) + { + %char = strcmp(getSubStr(%clientChallenge, %i, 1), ""); + if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97)) + { + %client.setDisconnectReason("Invalid characters in client challenge."); + %client.delete(); + return; + } + } + + // verify that the IP address the client thinks it is connecting to is the address this server + // is reasonable... take into account connections from the same private IP subnet (192.168.*.*, 10.*.*.*, etc) + %sanityIP = ipv4_hexBlockToIP(getSubStr(%clientChallenge, strLen(%clientChallenge) - 8, 8)); + %sourceIP = ipv4_hexBlockToIP(t2csri_gameClientHexAddress(%client)); + if (!ipv4_reasonableConnection(%sourceIP, %sanityIP)) + { + %client.setDisconnectReason("Potential man in the middle attack detected. Your client claims it connected to: " @ %sanityIP @ ", but the server does not consider this reasonable."); + %client.delete(); + return; + } + + // calculate a random 64-bit server side challenge + rubyEval("tsEval '$temp=\"' + rand(18446744073709551615).to_s(16) + '\";'"); + %client.t2csri_serverChallenge = $temp @ t2csri_gameClientHexAddress(%client); + + %fullChallenge = %client.t2csri_clientChallenge @ %client.t2csri_serverChallenge; + rubyEval("tsEval '$temp=\"' + rsa_mod_exp('" @ %fullChallenge @ "'.to_i(16), '" @ %e @ "'.to_i(16), '" @ %n @ "'.to_i(16)).to_s(16) + '\";'"); + + // send the challenge in 200 byte chunks + for (%i = 0; %i < strlen($temp); %i += 200) + { + commandToClient(%client, 't2csri_getChallengeChunk', getSubStr($temp, %i, 200)); + } + // tell the client we're done sending + commandToClient(%client, 't2csri_decryptChallenge'); + + // set up the "auth" info retrieved by cid.getAuthInfo() + %client.t2csri_authinfo = %user @ "\t\t0\t" @ %guid @ "\n0\n"; + + // clan support: check supplemental time limited certificate, if it was sent + %comCert = %client.t2csri_comCert; + if (strLen(%comCert) > 0) + { + // assuming there is a comCert, and we aren't running in bare mode + if (getField(%comCert, 3) $= %guid) + { + // GUID in the community cert matches that of the account cert + %client.t2csri_authinfo = %client.t2csri_comInfo; + } + else + { + // uh oh... someone's being naughty.. valid cert, but for a different player. kill them! + %client.setDisconnectReason("Community supplemental certificate doesn't match account certificate."); + %client.delete(); + return; + } + } +} + + +// verify the client's server challenge matches the one stored, if so, continue +// loading sequence +function serverCmdt2csri_challengeResponse(%client, %serverChallenge) +{ + if (%client.doneAuthenticating) + return; + + if (%client.t2csri_serverChallenge $= %serverChallenge) + { + // check to see if the client is GUID banned, now that we verified their certificate + if (banList_checkGUID(getField(%client.t2csri_authInfo, 3))) + { + %client.setDisconnectReason("You are not allowed to play on this server."); + %client.delete(); + return; + } + + // client checks out... continue loading sequence + %client.onConnect(%client.tname, %client.trgen, %client.tskin, %client.tvoic, %client.tvopi); + } + else + { + %client.setDisconnectReason("Invalid server challenge. Check your account key for corruption."); + %client.delete(); + } +} + +// delete a client if they spend more than 15 seconds authenticating +function t2csri_expireClient(%client) +{ + if (!isObject(%client)) + return; + %client.setDisconnectReason("This is a TribesNext server. You must install the TribesNext client to play. See www.tribesnext.com for info."); + %client.delete(); +} + +package t2csri_server +{ + // packaged to create the "pre-connection" authentication phase + function GameConnection::onConnect(%client, %name, %raceGender, %skin, %voice, %voicePitch) + { + if (%client.t2csri_serverChallenge $= "") + { + // check to see if the client is IP banned + if (banList_checkIP(%client)) + { + %client.setDisconnectReason("You are not allowed to play on this server."); + %client.delete(); + return; + } + + //echo("Client connected. Initializing pre-connection authentication phase..."); + // save these for later + %client.tname = %name; + %client.trgen = %raceGender; + %client.tskin = %skin; + %client.tvoic = %voice; + %client.tvopi = %voicePitch; + + // start the 15 second count down + %client.tterm = schedule(15000, 0, t2csri_expireClient, %client); + + commandToClient(%client, 't2csri_pokeClient', "T2CSRI 1.1 - 03/18/2009"); + return; + } + //echo("Client completed pre-authentication phase."); + + // continue connection process + if (isEventPending(%client.tterm)) + cancel(%client.tterm); + + Parent::onConnect(%client, %name, %raceGender, %skin, %voice, %voicePitch); + %client.doneAuthenticating = 1; + } + + // packaged to prevent game leaving messages for clients that are in the authentication phase + function GameConnection::onDrop(%client, %reason) + { + if (!isObject(%client) || !%client.doneAuthenticating) + return; + Parent::onDrop(%client, %reason); + } + + // packaged to pull info from the certificate, rather than some internal data structures + // format is kept consistent though: + // >Name ActiveClanTag Prepend(0)/Postpend(1)Tag guid + // >NumberOfClans + // >ClanName TagForClan Prepend(0)/Postpend(1)Tag clanid rank title + + // in this version, there is no clan support, so those fields are empty + // clan support will be implemented via delegation to a community server + function GameConnection::getAuthInfo(%client) + { + if (%client.getAddress() $= "Local" && %client.t2csri_authInfo $= "") + %client.t2csri_authInfo = WONGetAuthInfo(); + return %client.t2csri_authInfo; + } + + // deactivating old master list server protocol handlers in script + // sending a game type list to a dedicated server would result in a massive number + // of nuiscance calls to the following functions, and spam the console with pages of errors + // the errors were the main source of CPU utilization, so just setting stubs is adequate protection + function addGameType() + { + return; + } + function clearGameTypes() + { + return; + } + function clearMissionTypes() + { + return; + } + function sortGameAndMissionTypeLists() + { + return; + } +}; + +if ($PlayingOnline) + activatePackage(t2csri_server); diff --git a/t2csri/serverSideClans.cs b/t2csri/serverSideClans.cs new file mode 100644 index 0000000..2d1f100 --- /dev/null +++ b/t2csri/serverSideClans.cs @@ -0,0 +1,204 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 1.0: 2009-02-13 + +// A little bit of development theory: +// -The Apotheosis DLL contains 3 RSA public keys. One for authentication, one for updates, +// and one for delegation. The delegation key forms the root of the community system trust heirarchy. +// -The delegated-community-enhancement server issues time limited community certificates, which +// annotate the bare account certificates. The annotations include current name, current clan, current tag +// and current clan membership so that getAuthInfo() provides all relevant information. These certificates +// are time limited to enforce the "current" status of the annotations. +// -Since game servers don't communicate with centralized systems (except for listing), the client is +// responsible for providing a signed community certificate, and if prompted, the client is also +// responsible for providing the authoratatively signed certificate from the relevant DCE. Thus, the +// server will accumilate a small cache of valid DCE certificates. + +// DCE certificate format: +// DCEName DCENum IssuedEpoch ExpireEpoch 0 0 e n sig +// The two zeros are reserved for future use. +// Community certificate format: +// DCENum IssuedEpoch ExpireEpoch IssuedForGUID HexBlob Sig +// HexBlob format: +// (Follows same format as contents returned by getAuthInfo, but is hex encoded.) + +// verify with the delegation RSA public key... hard coded in the executable +function t2csri_verify_deleg_signature(%sig) +{ + %sig = strReplace(%sig, "\x27", "\\\x27"); + rubyEval("tsEval '$temp=\"' + t2csri_verify_deleg_signature('" @ %sig @ "').to_s(16) + '\";'"); + while (strLen($temp) < 40) + $temp = "0" @ $temp; + return $temp; +} + +// allow the client to send in an unknown DCE certificate +function serverCmdt2csri_getDCEChunk(%client, %chunk) +{ + // client can only send in one DCE + if (%client.t2csri_sentDCEDone) + return; + + %client.t2csri_activeDCE = %client.t2csri_activeDCE @ %chunk; + if (strlen(%client.t2csri_activeDCE) > 20000) + { + %client.setDisconnectReason("DCE certificate is too long."); + %client.delete(); + return; + } +} + +// client finished sending their DCE. validate it +function serverCmdt2csri_finishedDCE(%client) +{ + if (%client.t2csri_sentDCEDone) + return; + + %dce = %client.t2csri_activeDCE; + if (getFieldCount(%dce) != 9) + { + %client.setDisconnectReason("DCE certificate format is invalid."); + %client.delete(); + return; + } + %dceName = getField(%dce, 0); + %dceNum = getField(%dce, 1); + %dceIssued = getField(%dce, 2); + %dceExpire = getField(%dce, 3); + %dceE = getField(%dce, 6); + %dceN = getField(%dce, 7); + + // check to see if we already have this certificate + if ($T2CSRI::DCEE[%dceNum] !$= "") + { + // we already have the cert... set the client as done + %client.t2csri_sentDCEDone = 1; + %client.t2csri_activeDCE = ""; + return; + } + + %dceSig = getField(%dce, 8); + %sigSha = t2csri_verify_deleg_signature(%dceSig); + %sumStr = %dceName @ "\t" @ %dceNum @ "\t" @ %dceIssued @ "\t" @ %dceExpire @ "\t"; + %sumStr = %sumStr @ getField(%dce, 4) @ "\t" @ getField(%dce, 5) @ "\t" @ %dceE @ "\t" @ %dceN; + %calcSha = sha1sum(%sumStr); + + if (%sigSha !$= %calcSha) + { + %client.setDisconnectReason("DCE is not signed by authoritative root."); + %client.delete(); + return; + } + + // passed signature check... now check to see if it has expired/issued time has arrived + %currentTime = currentEpochTime(); + if (%currentTime < %dceIssued || %currentTime > %dceExpire) + { + %client.setDisconnectReason("DCE is not valid for the current time period."); + %client.delete(); + return; + } + + // passed time check... enter it into global data structure + $T2CSRI::DCEName[%dceNum] = %dceName; + $T2CSRI::DCEE[%dceNum] = %dceE; + $T2CSRI::DCEN[%dceNum] = %dceN; + + // client has successfully sent a DCE + %client.t2csri_sentDCEDone = 1; + %client.t2csri_activeDCE = ""; + + // client was pending on a certificate signature check, do that now that we have the DCE cert + if (%client.t2csri_pendingDCE) + { + %client.t2csri_pendingDCE = 0; + serverCmdt2csri_comCertSendDone(%client); + } +} + +// client sending community cert chunk +function serverCmdt2csri_sendCommunityCertChunk(%client, %chunk) +{ + // client can only send in one community cert + if (%client.t2csri_sentComCertDone) + return; + + %client.t2csri_comCert = %client.t2csri_comCert @ %chunk; + if (strlen(%client.t2csri_comCert) > 20000) + { + %client.setDisconnectReason("Community certificate is too long."); + %client.delete(); + return; + } +} + +// client has sent in a full community certificate... validate and parse it +function serverCmdt2csri_comCertSendDone(%client) +{ + if (%client.t2csri_sentComCertDone) + return; + + %comCert = %client.t2csri_comCert; + if (getFieldCount(%comCert) != 6) + { + %client.setDisconnectReason("Community certificate format is invalid."); + %client.delete(); + return; + } + + // parse + %dceNum = getField(%comCert, 0); + %issued = getField(%comCert, 1); + %expire = getField(%comCert, 2); + %guid = getField(%comCert, 3); + %blob = getField(%comCert, 4); + %sig = getField(%comCert, 5); + %sumStr = getFieldS(%comCert, 0, 4); + %calcSha = sha1Sum(%sumStr); + + // find the correct DCE + %e = $T2CSRI::DCEE[%dceNum]; + %n = $T2CSRI::DCEN[%dceNum]; + + // what if we don't have it? ask the client for a copy + if (%e $= "") + { + %client.t2csri_pendingDCE = 1; + commandToClient(%client, 't2csri_requestUnknownDCECert', %dceNum); + return; + } + + // get the signature SHA1 + rubyEval("tsEval '$temp = \"' + rsa_mod_exp('" @ %sig @ "'.to_i(16), '" @ %e @ "'.to_i(16), '" @ %n @ "'.to_i(16)).to_s(16) + '\";'"); + while (strlen($temp) < 40) + $temp = "0" @ $temp; + %sigSha = $temp; + + if (%sigSha !$= %calcSha) + { + %client.setDisconnectReason("Community cert is not signed by a known/valid DCE."); + %client.delete(); + return; + } + + // check expiration + %currentTime = currentEpochTime(); + if (%currentTime > %expire) + { + %client.setDisconnectReason("Community cert has expired. Get a fresh one from the DCE."); + %client.delete(); + return; + } + + // valid cert... set the field for processing in the auth-phase code + %len = strlen(%blob); + for (%i = 0; %i < %len; %i += 2) + { + %decoded = %decoded @ collapseEscape("\\x" @ getSubStr(%blob, %i, 2)); + } + %client.t2csri_comInfo = %decoded @ "\n"; + %client.t2csri_sentComCertDone = 1; +} \ No newline at end of file diff --git a/t2csri/serverglue.cs b/t2csri/serverglue.cs new file mode 100644 index 0000000..5e7db08 --- /dev/null +++ b/t2csri/serverglue.cs @@ -0,0 +1,23 @@ +// Tribes 2 Unofficial Authentication System +// http://www.tribesnext.com/ +// Written by Electricutioner/Thyth +// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative + +// Version 1.0 initialization and glue file (server side) + +if (isObject(ServerGroup)) +{ + // load the Ruby utils and cryptography module + exec("t2csri/rubyUtils.cs"); + rubyExec("t2csri/crypto.rb"); + + // load the torque script components + exec("t2csri/serverSide.cs"); + exec("t2csri/serverSideClans.cs"); + exec("t2csri/bans.cs"); + exec("t2csri/ipv4.cs"); + exec("t2csri/base64.cs"); + + // get the global IP for sanity testing purposes + schedule(32, 0, ipv4_getInetAddress); +}