2013年7月29日 星期一

自訂UnrealScript指令

UDN花了許多篇幅解釋UnrealScript底層的運作,不過卻沒提如何自訂新的指令。本篇將透過一個簡單的範例說明如何擴充UnrealScript的指令。

UnrealScript並未提供擴充機制,讓開發者可以在自己的專案裡撰寫程式碼自訂新的指令。雖然增加擴充機制不會太難,主要是讓FScriptCompiler和UByteCodeSerializer可以subclassing,不過由於不是這次的主題,所以本例中就直接修改底層的程式碼。

要自訂新的指令,大致上需要以下的步驟:
  • 找一個還未使用的位元碼(byte code)當作自訂指令碼
  • 如果這個指令需要使用一個新的名稱,可以在UnNames.h註冊一個新的名字
  • 修改UByteCodeSerializer使得新增的指令可以正確地序列化。
  • 撰寫對應的函式並且註冊到GNatives
  • 修改FScriptCompiler讓編譯器認得新的指令

範例


C++的TArray有AddUniqueItem()但是UnrealScript裡的陣列卻沒有支援,所以就以此為例。語法如下:
array_var.AddUniqueItem( element_expr )
由於語法和AddItem()一樣,所以只要稍加修改原本的程式碼,加上檢查是否重覆即可。

首先需要找到一個未使用的位元碼,然後在EExprToken宣告:
enum EExprToken
{
...
    EX_DynArrayAddUniqueItem= 0x4C,
...
};
AddUniqueItem這個名稱稍後解析時會用到,先在UnNames.h裡註冊一下:
REGISTER_NAME( 1208, AddUniqueItem )
接著修改序列化的部分,由SERIALIZEEXPR_INC這個巨集負責處理,會被UByteCodeSerializer::SerializeExpr()和UStruct::SerializeExpr()呼叫。由於語法和AddItem一樣,直接使用即可。
#ifdef SERIALIZEEXPR_INC
...
 case EX_DynArrayAddItem:
 case EX_DynArrayRemoveItem:
 case EX_DynArrayAddUniqueItem:
        {
            SerializeExpr( iCode, Ar ); // Array property expression
            XFER(CodeSkipSizeType);     // Number of bytes to skip if NULL context encountered

            TGuardValue<UClass*> RestoreContext(CurrentContext, NULL);
            SerializeExpr( iCode, Ar ); // Item expression
            break;
        }
...
從這邊可以看出位元碼序列的組成:
  1. EX_DynArrayAddUniqueItem
  2. 陣列本身
  3. 指令跳過長度(NULL時為了跳過指令需要前進的位元數)
  4. 要加入的元素
然後撰寫對應函式,基本上和AddItem一樣,只是還會檢查元素是否重覆。然後記得改用EX_DynArrayAddUniqueItem位元碼註冊此函式。
void UObject::execDynArrayAddUniqueItem( FFrame& Stack, RESULT_DECL )
{ 
    GProperty = NULL;
    GPropObject = this;
    Stack.Step( this, NULL );
    UArrayProperty* ArrayProp = Cast<UArrayProperty>(GProperty);
    FScriptArray* Array=(FScriptArray*)GPropAddr;
    
    // if GPropAddr is NULL, we weren't able to read a valid property address from the stream, which should mean that we evaluted a NULL
    // context expression (accessed none)
    if ( GPropAddr != NULL )
    {
        // advance past the word used to hold the skip offset - we won't need it
        Stack.Code += sizeof(CodeSkipSizeType);
        
        // figure out what type to read off the stack
        UProperty *InnerProp = ArrayProp->Inner;
        checkSlow(InnerProp != NULL);
        
        // grab the item
        BYTE *Item = (BYTE*)appAlloca(InnerProp->ElementSize);
        appMemzero(Item,InnerProp->ElementSize);
        Stack.Step(Stack.Object,Item);
        
        P_FINISH;
        
        UBOOL bUnique = TRUE;
        for(INT Idx=0; Idx<Array->Num(); Idx++)
        {
            if(InnerProp->Identical( (BYTE*)Array->GetData() + (Idx*InnerProp->ElementSize), Item ))
            {
                bUnique = FALSE;
                break;
            }
        }
        
        if( bUnique )
        {
            // add it to the array
            INT Index = Array->AddZeroed(1,InnerProp->ElementSize);
            InnerProp->CopySingleValue((BYTE*)Array->GetData() + (Index * InnerProp->ElementSize), Item);
            
            GUniqueItem = (BYTE*)Array->GetData() + (Index * InnerProp->ElementSize);
            
            if ( InnerProp->HasAnyPropertyFlags(CPF_NeedCtorLink) )
            {
                InnerProp->DestroyValue(Item);
            }
            
            // return the index of the added item
            *(INT*)Result = Index;
        }
        else
        {
            *(INT*)Result = INDEX_NONE;
        }
    }
    else
    {
        // accessed none while trying to evaluate the address of the array - skip over the parameter expression bytecodes
        CodeSkipSizeType NumBytesToSkip = Stack.ReadCodeSkipCount();
        Stack.Code += NumBytesToSkip;
        
        // set the return value to the expected "invalid" value
        *(INT*)Result = INDEX_NONE;
    }
}
IMPLEMENT_FUNCTION( UObject, EX_DynArrayAddUniqueItem, execDynArrayAddUniqueItem );
最後還要修改編譯的程式碼解析新的指令並且產生出對應的位元碼序列。由於語法和AddItem一樣,所以只要改掉指令代碼成AddUniqueItem即可。
UBOOL FScriptCompiler::CompileExpr(...)
{
...
            else if (MatchIdentifier(NAME_AddUniqueItem))
            {
                UArrayProperty *ArrayProp = Cast<UArrayProperty>(Prop);
                if (ArrayProp == NULL)
                {
                    ScriptErrorf(SCEL_Unknown/*SCEL_Expression*/, TEXT("Failed to find array property!"));
                }
                // make sure we're not violating any property flags
                if (Token.PropertyFlags & CPF_Const)
                {
                    ScriptErrorf(SCEL_Restricted, TEXT("Attempting to call 'AddUniqueItem' on const dynamic array [%s]"),Prop!=NULL?*Prop->GetName():TEXT("Unknown"));
                }
                FScriptLocation HighRetry;
                Writer << EX_DynArrayAddUniqueItem;
                MoveCompiledCode( StartOfExpression, HighRetry );
                
                // if this dynamic array operation is evaluated through a NULL context, we need to skip over the bytes for the parameters of 'AddUniqueItem'.
                // NullContextSkipOverLocation marks the spot where we'll insert the skipover data later
                FScriptLocation NullContextSkipOverBegin;
                
                // grab the item to add
                RequireSymbol( TEXT("("), TEXT("'adduniqueitem(...)'") );
                CompileExpr(FPropertyBase(ArrayProp->Inner, CPRT_AssignmentReference), TEXT("adduniqueitem(...)"));
                RequireSymbol( TEXT(")"), TEXT("'adduniqueitem(...)'") );
                
                Writer << EX_EndFunctionParms;
                EmitDebugInfo(DI_EFPOper);
                
                // now that the parameter expression has been compiled, we can go back and insert the correct value for the number of bytes to skip-over 
                // when the array is accessed through a NULL context
                {
                    // this marks the end of the bytecode corresponding to the parameter expressions; emit the number of bytecodes used by the expression/s,
                    // then move that skip offset so that it's read from the stream just after we read the array property
                    FScriptLocation NullContextSkipOverEnd;
                    CodeSkipSizeType wSkip = TopNode->Script.Num() - NullContextSkipOverBegin.CodeTop;
                    Writer << wSkip;
                    
                    MoveCompiledCode(NullContextSkipOverBegin, NullContextSkipOverEnd);
                }
                
                if ( TokenList )
                {
                    (*TokenList) += Token;
                }
                Token = FPropertyBase(CPT_Int);
                Token.ArrayDim = 1;
                
                // return index result
                GotAffector = 1;
                // clear AffectorReturnProperty so that we don't generate a EX_EatReturnValue if the expression we just compiled
                // was a function call whose return value requires destruction - we'll handle this manually
                AffectorReturnProperty = NULL;
                
                break;
            }
...
}

沒有留言:

張貼留言