bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 15, 2007 19:28:28 GMT
Having some problems with level restricted items. I thought I would show you what I'm doing and you may be able to point out my mistake. This ILR script works fine w/o Legendary Levels so I know it's not that. It's a tag based system so that whatever level I need to restrict it too I just put that into the tag name of the item; i.e. helm_50_
The Problem: When I restrict an item for any level beyond 40 it won't let a player use the item even if they meet the level requirements. For example, if I restrict a helmet for level 50 and above when a player puts it on that is 50+ they get the same message as those who don't meet the requirement and the helmet is automatically removed.
The Script: I made sure to include the hgll_const_inc but not sure where I'm going wrong. I've tried using the xp value instead of the level, but that didn't work. I tried using constant [XP_REQ_LVL50 = 1225000;] directly in the script and that didn't work. Any ideas would be greatly appreciated.
I tried to modify the script size in this post but the text remained small. Sorry about that.
#include "hgll_const_inc" #include "x2_inc_itemprop"
void ILR(object oPC, object oItem) {
int nILR = GetLocalInt(oItem, "ilr"); int nNumClass = GetLocalInt(oItem, "numclass");
(nNumClass));
if (nILR > 0) {
int nLoop, nClass; string sClass; int bILR = FALSE;
for (nLoop = 1; nLoop<=nNumClass && bILR == FALSE; nLoop++) {
nClass = GetLocalInt(oItem, "class" + IntToString(nLoop)); if (GetStringLength(sClass) > 0) { sClass = sClass + "or "; } sClass = sClass + Get2DAString("classes", "Label", nClass) + " ";
if (nILR <= GetLevelByClass(nClass, oPC)) { bILR = TRUE; } }
if (bILR == FALSE) { SendMessageToPC(oPC, "You must be level " + IntToString(nILR) + " or greater in " + sClass + " to use the item: " + GetName(oItem)); AssignCommand(oPC, ClearAllActions(TRUE)); AssignCommand(oPC, ActionUnequipItem(oItem)); } } }
void main() { int Test; string Tell; int Num = 1; object oItem = GetPCItemLastEquipped(); object oPC = GetPCItemLastEquippedBy(); string sTag = GetTag(oItem); int iLvl = GetHitDice(oPC); while (Num <= 50) { if (iLvl < Num) { Tell = IntToString(Num); string sString = "**_"+Tell+"_**"; Test = TestStringAgainstPattern(sString, sTag); if (Test == TRUE) { SendMessageToPC(oPC, "You must be level "+Tell+" or greater to use the item: "+GetName(oItem)); AssignCommand(oPC, ClearAllActions(TRUE)); AssignCommand(oPC, ActionUnequipItem(oItem)); return; } } Num++; }
ILR(oPC, oItem);
if (IPGetIsMeleeWeapon(oItem) == TRUE) DelayCommand(0.1, AssignCommand(oPC,ClearAllActions(TRUE))); }
I don't want to really use a method that uses item value because I'm using this with an upgraded version of Scarface's Socketed Items script. So a Level 50+ Helm would have a greater number of empty sockets than lower level helms. Empty sockets don't really have a value in terms of the .2da files.
|
|
|
Post by FunkySwerve on Jul 15, 2007 22:06:34 GMT
Legendary levels aren't real levels, they don't enable you to use items above level 40, only npcs do that, unless you make 2da edits. We use a 2da edit that raises the value cap of level 40 items by a ton, and then script our own ILR above that. You might also be able to use a value based system above 40, with further 2da edits, but it doesn't sound like that is what you want. The 2da edit is serverside, no hak or player download required. If you aren't useing ILR at all though, this wont be an issue.
So, you cannot use GetHitDice, that caps at 40 on pcs. You need to use this function, from hgll_func_inc:
int CheckLegendaryLevel(object oPC) { int nLevel = GetLootable(oPC); if (nLevel<41) { return GetHitDice(oPC); } else { return nLevel; } }
It returns Hit Dice if the character has no legendary levels. Otherwise it returns their 'level' including legendary levels, which is stored as their Lootable.
Funky
|
|
bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 15, 2007 23:39:15 GMT
Thanks Funky,
I'm not sure where I need to place the code you gave.
I keep getting "parsing variable list"
One idea I had was a script that just used the XP value instead of "levels". I'm not sure if that would work though or how to write it.
|
|
|
Post by FunkySwerve on Jul 16, 2007 0:04:19 GMT
Just include hgll_func_inc, the function is in there. Then just use CheckLegendaryLevel instead of GetHItDice when you want to know levels including legendary level. Funky
|
|
bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 16, 2007 0:34:09 GMT
Ok, I'm feeling stupid. I'm getting UNKNOWN STATE IN COMPILER on Line 52.
Line 52 is int iLvl = CheckLegendaryLevel(object oPC);
here is the whole code that I have.
#include "hgll_const_inc" #include "hgll_func_inc" #include "x2_inc_itemprop"
void ILR(object oPC, object oItem) {
int nILR = GetLocalInt(oItem, "ilr"); int nNumClass = GetLocalInt(oItem, "numclass");
// SendMessageToPC(oPC, "ilr=" + IntToString(nILR)); // SendMessageToPC(oPC, "nNumClass=" + IntToString(nNumClass));
if (nILR > 0) {
int nLoop, nClass; string sClass; int bILR = FALSE;
for (nLoop = 1; nLoop<=nNumClass && bILR == FALSE; nLoop++) {
nClass = GetLocalInt(oItem, "class" + IntToString(nLoop)); if (GetStringLength(sClass) > 0) { sClass = sClass + "or "; } sClass = sClass + Get2DAString("classes", "Label", nClass) + " ";
if (nILR <= GetLevelByClass(nClass, oPC)) { bILR = TRUE; } }
if (bILR == FALSE) { SendMessageToPC(oPC, "You must be level " + IntToString(nILR) + " or greater in " + sClass + " to use the item: " + GetName(oItem)); AssignCommand(oPC, ClearAllActions(TRUE)); AssignCommand(oPC, ActionUnequipItem(oItem)); } } }
void main() { int Test; string Tell; int Num = 1; object oItem = GetPCItemLastEquipped(); object oPC = GetPCItemLastEquippedBy(); string sTag = GetTag(oItem); int iLvl = CheckLegendaryLevel(object oPC); while (Num <= 50) { if (iLvl < Num) { Tell = IntToString(Num); string sString = "**_"+Tell+"_**"; Test = TestStringAgainstPattern(sString, sTag); if (Test == TRUE) { SendMessageToPC(oPC, "You must be level "+Tell+" or greater to use the item: "+GetName(oItem)); AssignCommand(oPC, ClearAllActions(TRUE)); AssignCommand(oPC, ActionUnequipItem(oItem)); return; } } Num++; }
ILR(oPC, oItem);
if (IPGetIsMeleeWeapon(oItem) == TRUE) DelayCommand(0.1, AssignCommand(oPC,ClearAllActions(TRUE))); }
I appreciate that you're helping me with this.
|
|
|
Post by Balduvard on Jul 16, 2007 1:59:50 GMT
That error message is usually the result of the compiler having no clue what you're trying to tell it. In this case, you're trying to define a function in the middle of your main function. Fix: do not use the variable type (object, int, float, etc.) when specifying the variable used by a function inside the main function. That should fix the state issue. If you run into similar issues, it's always a good idea to check the Quick Fix List for direction. It may not come to you immediately, but if you sift through the code (particularly around the line of error) then the solution will present itself.
|
|
|
Post by FunkySwerve on Jul 16, 2007 3:19:32 GMT
I also keep a copy of Axe Murderer's script errors listing saved as a script template, so I can look at it in the script editor:
NWN Script Compiler Error Messages This is a list of some reasons why the compiler will produce certain error messages. When the compiler outputs an error message, it will identify the line number in the script where the error was detected. Sometimes the code mistake that is causing the message is actually located on the line preceding the one identified in the message. The location of the mistake is most likely on the identified line or the preceding line. It is rare that the mistake is elsewhere in the script but it can happen, particularly when the mistake involves the curly braces '{' or '}' that are used to delineate sections of the script called code-blocks. The compiler will never spit out an error identifying some line when the actual mistake is located on a line past the line number identified in the error message. The code mistake will always be either on the identified line or before it.
ARITHMETIC OPERATOR HAS INVALID OPERANDS - trying to use an arithmetic operator to compute a value where the operands being used are not of the same datatype as in: string sMsg = "The number of months in a year is " + 12; the correct way to write this would be: string sMsg = "The number of months in a year is " + IntToString( 12);
COMPARISON TEST HAS INVALID OPERANDS - trying to compare two values or expressions that are not the same datatype.
CONST KEYWORD CANNOT BE USED ON NON GLOBAL VARIABLES - trying to define a constant inside a function implementation. Constants must be defined in the global scope. Example const int A_VALID_CONSTANT = 169; // Outside all other functions is correct.
void ACustomFunction() { const int AN_INVALID_CONSTANT = 5; // Inside a custom function is wrong. }
const int VALID_CONSTANT2 = 69; // Outside all other functions is correct.
void main() { const int AN_INVALID_CONSTANT = 96; // Inside the main function scope is wrong. }
const int VALID_CONSTANT3 = 842; // Global scope, so this is also correct.
DECLARATION DOES_NOT MATCH PARAMETERS - calling a function using a parameter value that doesn't match the datatype defined for that parameter by the function's declaration.
- calling a function that requires parameters where the number of parameters specified in the function call does not match the number of parameters required by the function.
DUPLICATE FUNCTION IMPLEMENTATION (MAIN) - every event script must have one and only one main function. This error indicates that the script has defined more than one...typically caused by using the "#include" directive to include a script that is not an include script but rather a regular event script with its own main function defined. Scripts written to be used as object event handlers must have a main function defined in them. Scripts written to be used by event handler scripts through the "#include" directive must not have a main function defined in them.
DUPLICATE FUNCTION IMPLEMENTATION (<Name>) - every custom function added to a script must be defined once and only once. This error indicates that the custom function designated by the function name <Name> has been defined two or more times in the script...typically caused by using the "#include" directive to include two scripts that each define a function named <Name> or to include a script that defines a function named <Name> when there is another definition for the <Name> function already defined in the current script.
ELLIPSIS IN IDENTIFIER - misspelling the #include directive by writing #included instead.
ERROR PARSING VARIABLE LIST - usually means there is a problem with one or more parameters in a function call.
- sometimes happens when a semicolon is missing at the end of a line.
- trying to define a variable whose name contains one or more invalid characters. As in: void main() { int iMy*Bad*Variable = 0; }
FUNCTION DEFINITION MISSING NAME - trying to define a constant or a global variable that has already been defined as a constant. As in: const int X; const float X; // variable X has already been defined as a constant. string X: // variable X has already been defined as a constant.
void main() { } interestingly, this compiles ok although I seriously question its validity int X; const int X; string Y; const flost Y;
void main() { float Z = Y; // this appears to be OK string W = Y; // but this gives MISMATCHED TYPES error. }
FUNCTION IMPLEMENTATION AND DEFINITION DIFFER - a function was prototyped but the parameter list of the implementation does not exactly match the parameter list in the prototype (the terms prototype and definition are synonymous in nwscript). As in: void MyFunction( int iNum); // prototype for MyFunction void MyFunction() // implementation for MyFunction with different parameter list { }
FUNCTION DEFINITION MISSING PARAMETER LIST - trying to define a function without specifying a parameter list. Even a function that requires no parameters must have an empty parameter list defined like all event script's main functions do. For example: void main // missing the empty parameter list ( ) { } or void MyFunction // No parameter list specified. { }
INVALID DECLARATION TYPE - trying to define a custom function without declaring the datatype of one or more of its parameter(s). As in: void MyFunction( nValue, string sName) // datatype of parameter nValue has not been specified. instead of void MyFunction( int nValue, string sName) // parameter nValue correctly defined.
MISMATCHED TYPES - trying to assign a value to a variable where the value's datatype does not match the variable's datatype.
MULTIPLE CASE CONSTANT STATEMENTS WITHIN SWITCH - writing a switch statement that has two or more case blocks using the same case constant value. As in this: void main() { int iNumber = d3(); switch( iNumber) { case 1: iNumber = 170; break; case 2: iNumber = 340; break; case 2: iNumber = 825; // Case 2 has already been specified. break; case 3: iNumber = -1; break; } }
or this: const int ANOTHER_2 = 2;
void main() { int iNumber = d3(); switch( iNumber) { case 1: iNumber = 170; break; case 2: iNumber = 340; break; case ANOTHER_2: iNumber = 825; // Case 2 has already been specified. break; case 3: iNumber = -1; break; } }
NO COLON AFTER CASE LABEL - writing a case block for a switch statement and forgetting to put a colon after the label for the block. The label is the constant identifier name or value that appears directly following the word case. This one will come up if you forget the colon altogether or if you accidentally put in a semi-colon or some other symbol instead. Example: void main() { int iNumber = d3(); switch( iNumber) { case 1 iNumber = 170; // Oops...no colon after the label 1. break; case CONSTANT_NAME ; iNumber = 340; // Typed in a semi-colon by mistake. break; } }
NO FUNCTION STARTINGCONDITIONAL() IN SCRIPT - trying to compile a script whose main function is defined using void main() in the TextAppearsWhen event slot of a conversation. The TextAppearsWhen event slot is the only place where the script's main function must be defined using int StartingConditional() instead of void main() and the function must also return a TRUE or FALSE value.
NON-INTEGER EXPRESSION WHERE INTEGER REQUIRED - an expression evaluating to a datatype other than integer was entered where an integer or an expression that evaluates to an integer value must be entered.
- a single = was used in a logical expression where a == should have been used.
NO LEFT BRACKET ON EXPRESSION - missing opening parenthesis '(' in an expression. As in: void main() { int iA = 3; int iB = Random( iA) +1; if iA == iB) // Missing opening parenthesis on the expression. { return; } }
NO RIGHT BRACKET ON EXPRESSION - misspelled function name, or missing closing parenthesis ')' in an expression.
- missing logical operator in an expression.
- calling a function which is defined in an include script without using the #include directive to include the script containing the function's implementation.
- trying to define a variable whose name starts with a number. All variable names must start with a non-numeric character.
NO SEMICOLON AFTER EXPRESSION - missing semicolon at the end of a line. The mistake is often found on the line immediately preceding the one identified in the error message.
NOT ALL CONTROL PATHS RETURN A VALUE - failing to return a value in a non-void returning custom function. As in: string MyFunction( int iValue) { if( iValue == 0) { return "0"; } else { return IntToString( iValue +1); } // Missing a return statement here. }
its also very common to get this when writing switch statements. As in: string MyFunction( int iValue) { switch( iValue) { case 0: return "The value is zero"; case 1: return "The value is one"; default: return "The value is " +IntToString( iValue); } // Missing a return statement here. Note that even though it may be impossible to get to // to this point, as in this example, a return is still required here. }
PARSING RETURN STATEMENT - caused by making two mistakes: returning a value in a void-returning function, AND leaving the semicolon off of the end of the statement. Do them both and this error message will appear. As in: void MyFunction( int iValue) { if( iValue == 0) { return; } else { return 7 // 2 mistakes: missing ; at the end and a value is returned for a void function. } }
- missing a semicolon at the end of a statement that ends with an expression can also cause this error.
RETURN TYPE AND FUNCTION TYPE MISMATCHED - missing return value in a return statement from a non-void returning function.
- returning a value whose datatype doesn't match the datatype that the function is defined to return in the function's declaration.
- specifying a value of any type in a return statement called from a void-returning function.
UNDEFINED IDENTIFIER (<Name>) - trying to call a function named <Name> that has no implementation defined. If a function has been prototyped (the terms prototype and definition are synonymous in nwscript) but no implementation for it exists or the spelling of the function name in the implementation was mistakenly written so it does not match with the prototype this error will appear with no line number and the <Name> will not be shown either. As in: void MyFunction(); // prototype for MyFunction void myFunction() // implementation for MyFunction with function name 'misspelled' { }
void main() { MyFunction(); // call made to a prototyped function with no existing implementation }
will produce this error with no line number and no <Name> listed in the error message.
UNEXPECTED END COMPOUND STATEMENT - missing a closing curly brace '}'.
UNKNOWN STATE IN COMPILER - missing opening '(' or closing ')' parenthesis in an expression.
- missing or extra closing curly brace '}'.
- missing opening curly brace '{'.
- specifying a floating point number less than 1 without prefixing it with a zero. As in: void main() { float fBadFloat = .55f; // This needs to be written 0.55f or 0.55 instead. }
- putting two operators next to each other as in: string sMsg = "Hello I am " ++ GetName( oPC);
- missing a value or condition in a logical expression where an extra logical operator has been specified. As in: if( (x == 3) || (x == 12) || )
- attempting to define a loop variable inside a for-statement (C, C++ style). As in: for( int x = 0; x < 10; x++) In nwscript the variable x must be defined before the for-statement.
- explicitly specifying parameter datatypes and/or trying to assign values to parameters within the parameter list when calling a function. Specifying parameters in this manner is only done when a function is defined or prototyped and must not be done when the function is used (called) from a script. As in: void main() { object aTrigger = GetNearestObjectByTag( string sTag = "NW_GOBLINGRPa", object oTarget = OBJECT_SELF, int nNth = 1); } Resolved by passing only the variable name to the function when called: void main() { string sTag = "NW_GOBLINGRPa"; object oTarget = OBJECT_SELF; int nNth = 1; object aTrigger=GetNearestObjectByTag( sTag, oTarget, nNth); // This is a legal call. } or by passing in values: void main() { object aTrigger=GetNearestObjectByTag( "NW_GOBLINGRPa", OBJECT_SELF, 1); } or some combination of the two: void main() { string sTag = "NW_GOBLINGRPa"; object aTrigger=GetNearestObjectByTag( sTag, OBJECT_SELF, 1); // This is OK. }
VARIABLE ALREADY USED WITHIN SCOPE - trying to redefine a variable in the same scope where it has already been defined. As in: int y; int y; // variable y already defined as global.
void main() { int x; int x; // variable x already defined in this code-block. }
VARIABLE DEFINED WITHOUT TYPE - referencing a variable that wasn't defined earlier in the script.
- attempting to reference a variable which was defined previously but in a code-block that has gone out of scope. As in: int i; for( i = 0; i < 10; i++) { int x = i *2; // variable x defined here will only exist between the two curly braces. } int y = x; // the variable x has gone out of scope and no longer exists at this point.
- misspelling a variable name or a constant.
Funky
|
|
bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 18, 2007 23:46:26 GMT
Even after getting it to compile it still wouldn't let me wear items if I met the level requirement.
After thinking about it for a bit I decided a different approach which worked. This is to restrict gear to "Level 50".
#include "x2_inc_itemprop"
void main() { object oItem = GetPCItemLastEquipped(); object oPC = GetPCItemLastEquippedBy(); string sTag = GetTag(oItem); { if (GetIsPC(oPC) && (GetXP(oPC) <= 2401199))
{ SendMessageToPC(oPC, "You must have 2401200 experience points or greater to use the item: "+GetName(oItem)); AssignCommand(oPC, ClearAllActions(TRUE)); AssignCommand(oPC, ActionUnequipItem(oItem)); return; } }
if (IPGetIsMeleeWeapon(oItem) == TRUE) DelayCommand(0.1, AssignCommand(oPC,ClearAllActions(TRUE))); }
I know it works, but it hasn't gone through a lot of testing. Can you see any areas of improvement for it?
|
|
|
Post by FunkySwerve on Jul 19, 2007 1:58:05 GMT
You need to provide more details. Did you make the 2da edit I discussed above? Or are you running without ILR on? What message does it give when you are prevented from equipping the item. Did you put debug code in the script to see if the script was even firing or if ILR was stopping it first? This is pretty far removed from the legendary level system itselfs, and really doesn't belong here, but I'm happy to help anyway, assuming you give me enough to work with. Funky
|
|
bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 19, 2007 11:25:38 GMT
I used the xp needed for level 50. If you try to equip the item and you don't have the xp needed it unequips the item and you get a message stating you need to be level 50 and tells you the xp needed. I didn't have to edit the 2da files at all.
The script itself doesn't have Legendary Level code within it, but can be used to restrict items on mods with legendary levels.
Also if you guys hadn't of given me that link of code errors I don't think I would have been able to figure out some of the problems I was running into. Greatly appreciated!
|
|
|
Post by FunkySwerve on Jul 19, 2007 14:10:17 GMT
I used the xp needed for level 50. If you try to equip the item and you don't have the xp needed it unequips the item and you get a message stating you need to be level 50 and tells you the xp needed. I didn't have to edit the 2da files at all. I'm trying to figure out why you had difficulty in the first place. Again, have you turned off ILR (Item Level Restriction)? And what 'level' does the item say it is in the toolset? In any event, are you using tag-based scripting? If you are not, and that is your full onequip script, you need to restrict it to only affect the items you want. If you ARE using tag-based scripting, and that's the script for a single item, you need to restrict it to the onequip event only. Unless you want players to be able to equip the items while running, you need a much more solid unequip sequence. Also, going by experience isn't very robust, and will entail a lot of individual coding. That's why we use an int-based system. Here's an example, which would go in your module onequip event (though you would have to add to it if you are using tag-based scripting). void ForceUnequip (object oTarget, object oItem) { if (!GetIsObjectValid(oTarget)) return; if (!GetIsObjectValid(GetArea(oTarget))) { DelayCommand(5.0, ForceUnequip(oTarget, oItem)); return; } if (GetIsDead(oTarget)) { ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectResurrection(), oTarget); ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectHeal(GetMaxHitPoints(oTarget)), oTarget); DelayCommand(0.1, ForceUnequip(oTarget, oItem)); } else { effect eImmob = EffectCutsceneImmobilize(); ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eImmob, oTarget, 0.1); AssignCommand(oTarget, ClearAllActions(TRUE)); AssignCommand(oTarget, ActionUnequipItem(oItem)); AssignCommand(oTarget, ActionDoCommand(SetCommandable(TRUE))); AssignCommand(oTarget, SetCommandable(FALSE)); } }
int CheckLevelIncludingLegendaryLevel(object oPC) { int nLevel = GetLootable(oPC); if (nLevel<41) return GetHitDice(oPC); else return nLevel; }
int GetItemIsUsable (object oItem, object oUser=OBJECT_SELF, int bFeedback=TRUE) { int nItemLevel = GetLocalInt(oItem, "ILR"); if (nItemLevel > CheckLevelIncludingLegendaryLevel(oUser)) { if (bFeedback) FloatingTextStringOnCreature("<cþ>You must be level " + IntToString(nItemLevel) + " to use that!</c>", oUser, FALSE); return FALSE; } return TRUE; }
void main() { object oPC = GetPCItemLastEquippedBy(); if (!GetIsPC(oPC) || GetIsDM(oPC)) return; object oItem = GetPCItemLastEquipped(); if (!GetItemIsUsable(oItem, oPC)) ForceUnequip(oPC, oItem); } Funky
|
|
bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 20, 2007 11:06:44 GMT
I'm not sure what you mean by having IRL turned on or off. I'm using tag based scripts. Whatever number I use as the tag, it uses that number as the level to check for.
When I tried the script you wrote above and put it in the onequip I commented out my tag based scripts and used the variables in the items. I used the INT (interger) variable in the items and put "ILR" as the name, INT as the type and 50 as the value. There are several items that this will be used on. Your script did check to see if I met the level requirement that was put in the variable of the item, but it still did not let me equip it if I met that variable.
|
|
|
Post by Balduvard on Jul 20, 2007 12:14:21 GMT
From your server's nwnplayer.ini:
|
|
|
Post by FunkySwerve on Jul 20, 2007 15:53:14 GMT
If you don't set Item Level Restriction to 0, it will not let you equip items above the value indicated in the 2da for the number of hit dice you have. That is why I am asking about the level of the item, AS SHOWN IN THE ITEM PROPERTIES, in the toolset. What Required Level is shown in the item's General tab, and what level are you? Scripted ILR will only prohibit otherwise allowable use, not allow use prohibited by the standard ILR setting. Funky
|
|
bogo
Neophyte
Posts: 11
|
Post by bogo on Jul 20, 2007 22:27:18 GMT
On my way home from work it hit me what you were talking about. It's funny how things come to you at odd times. The server settings have ILR set to zero so everything can be run through scripting. I think the reason it took me so long to figure out what you meant was because I set it to zero a long time ago and forgot all about it.
|
|