SpazmoJones Posted March 18, 2015 Share Posted March 18, 2015 Hi All I'm trying to make a custom soldier name mod that is gender and race aware. The idea is that you'd use a tool like the XCom name generator here:http://daiz.io/xcom-namegen/ You prefix the name with a "1" to indicate male, or "2" to indicate female e.g.1John Smith2Sally Jones1Bob You can also optionally add a 2nd digit to pick the race(0=cauc, 1=african, 2=asian, 3=hispanic)e.g.10John Travolta11Mike Tyson21Oprah etc. I've made the mod and the code looks right when decompiling the results with UE Explorer, but the game crashes on start up. I've also tried making very simple versions of the mod that just set a local variable but that also crashes. Hopefully I'm making some silly noob error here that you guys can find. Here's the mod: MOD_NAME=Gender and Race Aware Custom Name ListUPK_FILE=XComGame.upkAUTHOR=eclipse666DESCRIPTION=Specify the gender and race of each name in your custom name listOBJECT=XGCharacterGenerator.CreateTSoldier:AUTO [bEFORE_CODE] // GenerateName(kSoldier.kAppearance.iGender, kSoldier.iCountry, kSoldier.strFirstName, kSoldier.strLastName, kSoldier.kAppearance.iRace)1B 28 37 00 00 00 00 00 00 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 35 92 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 35 97 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 16 [AFTER_CODE]// GenerateName(kSoldier.kAppearance.iGender, kSoldier.iCountry, kSoldier.strFirstName, kSoldier.strLastName, kSoldier.kAppearance.iRace)1B 28 37 00 00 00 00 00 00 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 35 92 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 35 97 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 16 //Add these commands after: // iCountry var will hold gender// Gender is first character of surname 1=Male 2=Female// iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); // remove the first char from the surname if gender was specified// kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName;// set the gender if it's specified in the name// kSoldier.kAppearance.iGender = iCountry > 0 ? iCountry : kSoldier.kAppearance.iGender; //***************DO GENDER*********************************************// iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); 0F // Let00 EB B9 00 00 // iCountry38 4A // StringToIntToken7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName2C 00 // byte 02C 01 // byte 1 16 // end parameters // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 45 // Conditional token "?"97 // ">"00 EB B9 00 00 // iCountry2C 00 // byte 016 // end parameters 14 00 // size of true result 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName2C 01 // byte 1 16 // end parameters // size of false result10 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName // set the gender if it's specified in the name// kSoldier.kAppearance.iGender = iCountry > 0 ? iCountry : kSoldier.kAppearance.iGender; 0F // Let35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 // kSoldier.kAppearance.iGender 45 // Conditional token "?" 97 // ">"00 EB B9 00 00 // iCountry2C 00 // byte 016 // end parameters05 00 // size of true result00 EB B9 00 00 // iCountry1B 00 // size of false result 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 // kSoldier.kAppearance.iGender //***************DO RACE********************************************* /*// iCountry var will now hold race// Race is (optional) 2nd character of surname 0=Caucasian 1=African 2=Asian 3=Hispanic // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); // set the race if it's specified in the name// kSoldier.kAppearance.iRace = iCountry > 0 ? iCountry : kSoldier.kAppearance.iRace; // remove the first char (now the second) from the surname if race was specified// kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let00 EB B9 00 00 // iCountry38 4A // StringToIntToken7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName2C 00 // byte 02C 01 // byte 1 16 // end parameters // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 45 // Conditional token "?"97 // ">"00 EB B9 00 00 // iCountry2C 00 // byte 016 // end parameters 14 00 // size of true result 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName2C 01 // byte 1 16 // end parameters // size of false result10 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName // set the race if it's specified in the name// kSoldier.kAppearance.iRace = iCountry > 0 ? iCountry : kSoldier.kAppearance.iRace; 0F // Let35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.kAppearance.iRace 45 // Conditional token "?" 97 // ">"00 EB B9 00 00 // iCountry2C 00 // byte 016 // end parameters05 00 // size of true result00 EB B9 00 00 // iCountry1B 00 // size of false result35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.kAppearance.iRace // the decompiled results with UE Explorer look correct to me: GenerateName(kSoldier.kAppearance.iGender, kSoldier.iCountry, kSoldier.strFirstName, kSoldier.strLastName, kSoldier.kAppearance.iRace); iCountry = int(Mid(kSoldier.strLastName, 0, 1)); kSoldier.strLastName = ((iCountry > 0) ? Mid(kSoldier.strLastName, 1) : kSoldier.strLastName); kSoldier.kAppearance.iGender = ((iCountry > 0) ? iCountry : kSoldier.kAppearance.iGender);... Link to comment Share on other sites More sharing options...
SpazmoJones Posted March 19, 2015 Author Share Posted March 19, 2015 (edited) I'm still struggling with this - it still crashes on startup. What's strange is that the last few lines of code that should be there don't appear when I decompile the patched file with UE Explorer. The last lines of the modified CreateTSoldier function look like: } GenerateName(kSoldier.kAppearance.iGender, kSoldier.iCountry, kSoldier.strFirstName, kSoldier.strLastName, kSoldier.kAppearance.iRace); kSoldier.kAppearance.iArmorTint = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.DEFENDER_MEDIKIT - 1; // End:0x143D if(XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.COUNCIL_STAT_BONUS >= 0) { kSoldier.kAppearance.iArmorDeco = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.COUNCIL_STAT_BONUS - 1; } // End:0x1541 if(XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.ShowUFOsOnMission >= 1) { kSoldier.kAppearance.iHaircut = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.ShowUFOsOnMission; } // my added code is here: iCountry = int(Mid(kSoldier.strLastName, 0, 1)); kSoldier.strLastName = ((iCountry > 0) ? Mid(kSoldier.strLastName, 1) : kSoldier.strLastName); kSoldier.kAppearance.iGender = ((iCountry > 0) ? iCountry : kSoldier.kAppearance.iGender); iCountry = int(Mid(kSoldier.strLastName, 0, 1)); kSoldier.strLastName = ((iCountry > 0) ? Mid(kSoldier.strLastName, 1) : kSoldier.strLastName); // there should be a line here with // kSoldier.kAppearance.iRace = ((iCountry > 0) ? iCountry : kSoldier.kAppearance.iRace); // the "return kSoldier" is missing too } My current mod looks like this. I suspect the problem is to do with memory sizes or pointers not being updated or something like that. Can somebody help please? MOD_NAME=Gender and Race Aware Custom Name List UPK_FILE=XComGame.upk AUTHOR=eclipse666 DESCRIPTION=Specify the gender and race of each name in your custom name list OBJECT=XGCharacterGenerator.CreateTSoldier:AUTO [BEFORE_CODE] // return ksoldier 04 00 E7 B9 00 00 0B 0B 0B 04 3A E8 B9 00 00 [AFTER_CODE] // iCountry var will hold gender // Gender is first character of surname 1=Male 2=Female // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); // remove the first char from the surname if gender was specified // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; // set the gender if it's specified in the name // kSoldier.kAppearance.iGender = iCountry > 0 ? iCountry : kSoldier.kAppearance.iGender; // DO GENDER // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); 0F // Let 00 EB B9 00 00 // iCountry 38 4A // StringToIntToken 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 00 // byte 0 2C 01 // byte 1 16 // end parameters // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 14 00 // size of true result 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 01 // byte 1 16 // end parameters // size of false result 10 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName // set the gender if it's specified in the name // kSoldier.kAppearance.iGender = iCountry > 0 ? iCountry : kSoldier.kAppearance.iGender; 0F // Let 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 // kSoldier.kAppearance.iGender 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 05 00 // size of true result 00 EB B9 00 00 // iCountry 1B 00 // size of false result 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 // kSoldier.kAppearance.iGender //===========DO RACE==================== // iCountry var will now hold race // Race is (optional) 2nd character of surname 0=Caucasian 1=African 2=Asian 3=Hispanic // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); // set the race if it's specified in the name // kSoldier.kAppearance.iRace = iCountry > 0 ? iCountry : kSoldier.kAppearance.iRace; // remove the first char (now the second) from the surname if race was specified // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 00 EB B9 00 00 // iCountry 38 4A // StringToIntToken 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 00 // byte 0 2C 01 // byte 1 16 // end parameters // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 14 00 // size of true result 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 01 // byte 1 16 // end parameters // size of false result 10 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName // set the race if it's specified in the name // kSoldier.kAppearance.iRace = iCountry > 0 ? iCountry : kSoldier.kAppearance.iRace; 0F // Let 35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.kAppearance.iRace 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 05 00 // size of true result 00 EB B9 00 00 // iCountry 1B 00 // size of false result 35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.kAppearance.iRace 04 00 E7 B9 00 00 0B 0B 0B 04 3A E8 B9 00 00 Edited March 19, 2015 by SpazmoJones Link to comment Share on other sites More sharing options...
wghost81 Posted March 19, 2015 Share Posted March 19, 2015 You forgot to adjust script memory size. REL_OFFSET=0x28 [MODDED_HEX] XX XX XX XX YY YY YY YY XX XX XX XX here sets memory size and YY YY YY YY - serial size. Alternatively you can use this code: REL_OFFSET=0x28 UNSIGNED=0xAAAAAAAA 0xAAAAAAAA here is script memory size in hex representation. In UEE go to View -> View Tokens. Scroll down to the last one to see something similar to (AAA/BBB) [53] EOS(1/1) Memory size of the script is equal to AAA+1 and serial size is equal to BBB+1. Note that both are in hex. If there is no EOS token at the end of your script, it means that current memory size is too small. Increase it to some random big value, like twice of the current value, and try again. Link to comment Share on other sites More sharing options...
SpazmoJones Posted March 19, 2015 Author Share Posted March 19, 2015 (edited) Hi wghost81 - thanks for the info. Is offset 0x28 the standard location where the script memory size is stored? I don't really understand how these mods work - I've been piecing together info by examining other mods and by reading everything I could find on the wiki. It's still pretty confusing though. I've changed the mod as follows and now I can see all my code when it's decompiled. XCom still crashes when it starts up though. Is the problem related to me not using named references to the various variables and structures or doesn't that matter? I set the script size to 0x1858 which is quite a bit larger than it needs to be. It doesn't have to be exact does it? MOD_NAME=Gender and Race Aware Custom Name List UPK_FILE=XComGame.upk AUTHOR=eclipse666 DESCRIPTION=Specify the gender and race of each name in your custom name list //EXPAND_FUNCTION=XGCharacterGenerator.CreateTSoldier:3936 OBJECT=XGCharacterGenerator.CreateTSoldier:AUTO REL_OFFSET=0x28 UNSIGNED=0x1858 [BEFORE_CODE] // return ksoldier 04 00 <.kSoldier> [AFTER_CODE] // iCountry var will hold gender // Gender is first character of surname 1=Male 2=Female // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); // remove the first char from the surname if gender was specified // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; // set the gender if it's specified in the name // kSoldier.kAppearance.iGender = iCountry > 0 ? iCountry : kSoldier.kAppearance.iGender; // DO GENDER // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); 0F // Let 00 EB B9 00 00 // iCountry 38 4A // StringToIntToken 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 00 // byte 0 2C 01 // byte 1 16 // end parameters // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 14 00 // size of true result 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 01 // byte 1 16 // end parameters // size of false result 10 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName // set the gender if it's specified in the name // kSoldier.kAppearance.iGender = iCountry > 0 ? iCountry : kSoldier.kAppearance.iGender; 0F // Let 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 // kSoldier.kAppearance.iGender 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 05 00 // size of true result 00 EB B9 00 00 // iCountry 1B 00 // size of false result 35 82 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 00 00 E7 B9 00 00 // kSoldier.kAppearance.iGender //===========DO RACE==================== // iCountry var will now hold race // Race is (optional) 2nd character of surname 0=Caucasian 1=African 2=Asian 3=Hispanic // iCountry = int( Mid( kSoldier.strLastName, 0, 1 ) ); // set the race if it's specified in the name // kSoldier.kAppearance.iRace = iCountry > 0 ? iCountry : kSoldier.kAppearance.iRace; // remove the first char (now the second) from the surname if race was specified // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 00 EB B9 00 00 // iCountry 38 4A // StringToIntToken 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 00 // byte 0 2C 01 // byte 1 16 // end parameters // kSoldier.strLastName = iCountry > 0 ? Mid( kSoldier.strLastName, 1 ) : kSoldier.strLastName; 0F // Let 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 14 00 // size of true result 7F // Mid function 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName 2C 01 // byte 1 16 // end parameters // size of false result 10 00 35 96 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.strLastName // set the race if it's specified in the name // kSoldier.kAppearance.iRace = iCountry > 0 ? iCountry : kSoldier.kAppearance.iRace; 0F // Let 35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.kAppearance.iRace 45 // Conditional token "?" 97 // ">" 00 EB B9 00 00 // iCountry 2C 00 // byte 0 16 // end parameters 05 00 // size of true result 00 EB B9 00 00 // iCountry 1B 00 // size of false result 35 81 0F 00 00 84 0F 00 00 00 00 35 8E 0F 00 00 99 0F 00 00 00 01 00 E7 B9 00 00 // kSoldier.kAppearance.iRace 04 00 <.kSoldier> The end of the decompiled function looks like this now: GenerateName(kSoldier.kAppearance.iGender, kSoldier.iCountry, kSoldier.strFirstName, kSoldier.strLastName, kSoldier.kAppearance.iRace); kSoldier.kAppearance.iArmorTint = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.DEFENDER_MEDIKIT - 1; // End:0x143D if(XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.COUNCIL_STAT_BONUS >= 0) { kSoldier.kAppearance.iArmorDeco = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.COUNCIL_STAT_BONUS - 1; } // End:0x1541 if(XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.ShowUFOsOnMission >= 1) { kSoldier.kAppearance.iHaircut = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.ShowUFOsOnMission; } // MY CODE APPEARS HERE - YAY! But it still crashes on startup! :( iCountry = int(Mid(kSoldier.strLastName, 0, 1)); kSoldier.strLastName = ((iCountry > 0) ? Mid(kSoldier.strLastName, 1) : kSoldier.strLastName); kSoldier.kAppearance.iGender = ((iCountry > 0) ? iCountry : kSoldier.kAppearance.iGender); iCountry = int(Mid(kSoldier.strLastName, 0, 1)); kSoldier.strLastName = ((iCountry > 0) ? Mid(kSoldier.strLastName, 1) : kSoldier.strLastName); kSoldier.kAppearance.iRace = ((iCountry > 0) ? iCountry : kSoldier.kAppearance.iRace); return kSoldier; //return ReturnValue; } Edited March 19, 2015 by SpazmoJones Link to comment Share on other sites More sharing options...
wghost81 Posted March 20, 2015 Share Posted March 20, 2015 (edited) >I set the script size to 0x1858 which is quite a bit larger than it needs to be. It doesn't have to be exact does it? Quite opposite: it has to be exact value or the game will crash. Random big value allows you to decompile the script with UEE and obtain the exact memory size value. As I wrote earlier, now you have to open your script in UEE and go to View -> View Tokens. Scroll down to the last line to see something similar to(AAA/BBB) [53] EOS(1/1) Memory size of the script is equal to AAA+1 and serial size is equal to BBB+1. Note that both are in hex. Yes, for all XCOM scripts 0x28 is a standard memory size offset, you can use it safely. Edited March 20, 2015 by wghost81 Link to comment Share on other sites More sharing options...
Recommended Posts