/* ** thingdef.cpp ** ** Actor definitions ** **--------------------------------------------------------------------------- ** Copyright 2002-2008 Christoph Oelckers ** Copyright 2004-2008 Randy Heit ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** 4. When not used as part of ZDoom or a ZDoom derivative, this code will be ** covered by the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or (at ** your option) any later version. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ #include "gi.h" #include "actor.h" #include "r_defs.h" #include "a_pickups.h" #include "cmdlib.h" #include "p_lnspec.h" #include "decallib.h" #include "p_local.h" #include "a_weapons.h" #include "p_conversation.h" #include "v_text.h" #include "backend/codegen.h" #include "stats.h" #include "info.h" #include "thingdef.h" // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- void InitThingdef(); // STATIC FUNCTION PROTOTYPES -------------------------------------------- static TMap StateSourceLines; static FScriptPosition unknownstatesource("unknown file", 0); //========================================================================== // // PClassActor :: Finalize // // Installs the parsed states and does some sanity checking // //========================================================================== void FinalizeClass(PClass *ccls, FStateDefinitions &statedef) { if (!ccls->IsDescendantOf(NAME_Actor)) return; auto cls = static_cast(ccls); try { statedef.FinishStates(cls); } catch (CRecoverableError &) { statedef.MakeStateDefines(nullptr); throw; } auto def = GetDefaultByType(cls); statedef.InstallStates(cls, def); statedef.MakeStateDefines(nullptr); if (cls->IsDescendantOf(NAME_Inventory)) { def->flags |= MF_SPECIAL; } if (cls->IsDescendantOf(NAME_Weapon)) { FState *ready = def->FindState(NAME_Ready); FState *select = def->FindState(NAME_Select); FState *deselect = def->FindState(NAME_Deselect); FState *fire = def->FindState(NAME_Fire); auto TypeName = cls->TypeName; // Consider any weapon without any valid state abstract and don't output a warning // This is for creating base classes for weapon groups that only set up some properties. if (ready || select || deselect || fire) { if (!ready) { I_Error("Weapon %s doesn't define a ready state.", TypeName.GetChars()); } if (!select) { I_Error("Weapon %s doesn't define a select state.", TypeName.GetChars()); } if (!deselect) { I_Error("Weapon %s doesn't define a deselect state.", TypeName.GetChars()); } if (!fire) { I_Error("Weapon %s doesn't define a fire state.", TypeName.GetChars()); } } } } //========================================================================== // // Saves the state's source lines for error messages during postprocessing // //========================================================================== void SaveStateSourceLines(FState *firststate, TArray &positions) { for (unsigned i = 0; i < positions.Size(); i++) { StateSourceLines[firststate + i] = positions[i]; } } FScriptPosition & GetStateSource(FState *state) { auto check = StateSourceLines.CheckKey(state); return check ? *check : unknownstatesource; } //========================================================================== // // SetImplicitArgs // // Adds the parameters implied by the function flags. // //========================================================================== void SetImplicitArgs(TArray *args, TArray *argflags, TArray *argnames, PContainerType *cls, uint32_t funcflags, int useflags) { // Must be called before adding any other arguments. assert(args == nullptr || args->Size() == 0); assert(argflags == nullptr || argflags->Size() == 0); if (funcflags & VARF_Method) { // implied self pointer if (args != nullptr) args->Push(NewPointer(cls, !!(funcflags & VARF_ReadOnly))); if (argflags != nullptr) argflags->Push(VARF_Implicit | VARF_ReadOnly); if (argnames != nullptr) argnames->Push(NAME_self); } if (funcflags & VARF_Action) { assert(!(funcflags & VARF_ReadOnly)); // implied caller and callingstate pointers if (args != nullptr) { // Special treatment for weapons and CustomInventory flagged functions: 'self' is not the defining class but the actual user of the item, so this pointer must be of type 'Actor' if (useflags & (SUF_WEAPON|SUF_ITEM)) { args->Insert(0, NewPointer(RUNTIME_CLASS(AActor))); // this must go in before the real pointer to the containing class. } else { args->Push(NewPointer(cls)); } args->Push(NewPointer(NewStruct("FStateParamInfo", nullptr))); } if (argflags != nullptr) { argflags->Push(VARF_Implicit | VARF_ReadOnly); argflags->Push(VARF_Implicit | VARF_ReadOnly); } if (argnames != nullptr) { argnames->Push(NAME_invoker); argnames->Push(NAME_stateinfo); } } } //========================================================================== // // CreateAnonymousFunction // // Creates a function symbol for an anonymous function // This contains actual info about the implied variables which is needed // during code generation. // //========================================================================== PFunction *CreateAnonymousFunction(PContainerType *containingclass, PType *returntype, int flags) { TArray rets; TArray args; TArray argflags; TArray argnames; // Functions that only get flagged for actors do not need the additional two context parameters. int fflags = (flags& (SUF_OVERLAY | SUF_WEAPON | SUF_ITEM)) ? VARF_Action | VARF_Method : VARF_Method; // [ZZ] give anonymous functions the scope of their class // (just give them VARF_Play, whatever) fflags |= VARF_Play; if (returntype) rets.Push(returntype); SetImplicitArgs(&args, &argflags, &argnames, containingclass, fflags, flags); PFunction *sym = Create(containingclass, NAME_None); // anonymous functions do not have names. sym->AddVariant(NewPrototype(rets, args), argflags, argnames, nullptr, fflags, flags); return sym; } //========================================================================== // // FindClassMemberFunction // // Looks for a name in a class's symbol table and outputs appropriate messages // //========================================================================== PFunction *FindClassMemberFunction(PContainerType *selfcls, PContainerType *funccls, FName name, FScriptPosition &sc, bool *error, const VersionInfo &version) { // Skip ACS_NamedExecuteWithResult. Anything calling this should use the builtin instead. if (name == NAME_ACS_NamedExecuteWithResult) return nullptr; PSymbolTable *symtable; auto symbol = selfcls->Symbols.FindSymbolInTable(name, symtable); auto funcsym = dyn_cast(symbol); if (symbol != nullptr) { auto cls_ctx = PType::toClass(funccls); auto cls_target = funcsym ? PType::toClass(funcsym->OwningClass) : nullptr; if (funcsym == nullptr) { if (PClass::FindClass(name)) return nullptr; // Special case when a class's member variable hides a global class name. This should still work. sc.Message(MSG_ERROR, "%s is not a member function of %s", name.GetChars(), selfcls->TypeName.GetChars()); } else if ((funcsym->Variants[0].Flags & VARF_Private) && symtable != &funccls->Symbols) { // private access is only allowed if the symbol table belongs to the class in which the current function is being defined. sc.Message(MSG_ERROR, "%s is declared private and not accessible", symbol->SymbolName.GetChars()); } else if ((funcsym->Variants[0].Flags & VARF_Protected) && symtable != &funccls->Symbols && (!cls_ctx || !cls_target || !cls_ctx->Descriptor->IsDescendantOf(cls_target->Descriptor))) { sc.Message(MSG_ERROR, "%s is declared protected and not accessible", symbol->SymbolName.GetChars()); } else if ((funcsym->Variants[0].Flags & VARF_Deprecated) && funcsym->mVersion <= version) { sc.Message(MSG_WARNING, "Call to deprecated function %s", symbol->SymbolName.GetChars()); } } // return nullptr if the name cannot be found in the symbol table so that the calling code can do other checks. return funcsym; } //========================================================================== // // CreateDamageFunction // // creates a damage function from the given expression // //========================================================================== void CreateDamageFunction(PNamespace *OutNamespace, const VersionInfo &ver, PClassActor *info, AActor *defaults, FxExpression *id, bool fromDecorate, int lumpnum) { if (id == nullptr) { defaults->DamageFunc = nullptr; } else { auto dmg = new FxReturnStatement(new FxIntCast(id, true), id->ScriptPosition); auto funcsym = CreateAnonymousFunction(info->VMType, TypeSInt32, 0); defaults->DamageFunc = FunctionBuildList.AddFunction(OutNamespace, ver, funcsym, dmg, FStringf("%s.DamageFunction", info->TypeName.GetChars()), fromDecorate, -1, 0, lumpnum); } } //========================================================================== // // CheckForUnsafeStates // // Performs a quick analysis to find potentially bad states. // This is not perfect because it cannot track jumps by function. // For such cases a runtime check in the relevant places is also present. // //========================================================================== static void CheckForUnsafeStates(PClassActor *obj) { static ENamedName weaponstates[] = { NAME_Ready, NAME_Deselect, NAME_Select, NAME_Fire, NAME_AltFire, NAME_Hold, NAME_AltHold, NAME_Flash, NAME_AltFlash, NAME_None }; static ENamedName pickupstates[] = { NAME_Pickup, NAME_Drop, NAME_Use, NAME_None }; TMap checked; ENamedName *test; auto cwtype = PClass::FindActor(NAME_Weapon); if (obj->IsDescendantOf(cwtype)) { if (obj->Size == cwtype->Size) return; // This class cannot have user variables. test = weaponstates; } else { auto citype = PClass::FindActor(NAME_CustomInventory); if (obj->IsDescendantOf(citype)) { if (obj->Size == citype->Size) return; // This class cannot have user variables. test = pickupstates; } else return; // something else derived from StateProvider. We do not know what this may be. } for (; *test != NAME_None; test++) { FState *state = obj->FindState(*test); while (state != nullptr && checked.CheckKey(state) == nullptr) // have we checked this state already. If yes, we can stop checking the current chain. { checked[state] = true; if (state->ActionFunc && state->ActionFunc->Unsafe) { // If an unsafe function (i.e. one that accesses user variables) is being detected, print a warning once and remove the bogus function. We may not call it because that would inevitably crash. GetStateSource(state).Message(MSG_ERROR, TEXTCOLOR_RED "Unsafe state call in state %s which accesses user variables, reached by %s.%s.\n", FState::StaticGetStateName(state).GetChars(), obj->TypeName.GetChars(), FName(*test).GetChars()); } state = state->NextState; } } } //========================================================================== // // CheckStates // // Checks if states link to ones with proper restrictions // Checks that all base labels refer a string with proper restrictions. // For these cases a runtime check in the relevant places is also present. // //========================================================================== static void CheckLabel(PClassActor *obj, FStateLabel *slb, int useflag, FName statename, const char *descript) { auto state = slb->State; if (state != nullptr) { if (!(state->UseFlags & useflag)) { GetStateSource(state).Message(MSG_ERROR, TEXTCOLOR_RED "%s references state %s as %s state, but this state is not flagged for use as %s.\n", obj->TypeName.GetChars(), FState::StaticGetStateName(state).GetChars(), statename.GetChars(), descript); } } if (slb->Children != nullptr) { for (int i = 0; i < slb->Children->NumLabels; i++) { auto state = slb->Children->Labels[i].State; CheckLabel(obj, &slb->Children->Labels[i], useflag, statename, descript); } } } static void CheckStateLabels(PClassActor *obj, ENamedName *test, int useflag, const char *descript) { FStateLabels *labels = obj->GetStateLabels(); for (; *test != NAME_None; test++) { auto label = labels->FindLabel(*test); if (label != nullptr) { CheckLabel(obj, label, useflag, *test, descript); } } } static void CheckStates(PClassActor *obj) { static ENamedName actorstates[] = { NAME_Spawn, NAME_See, NAME_Melee, NAME_Missile, NAME_Pain, NAME_Death, NAME_Wound, NAME_Raise, NAME_Yes, NAME_No, NAME_Greetings, NAME_None }; static ENamedName weaponstates[] = { NAME_Ready, NAME_Deselect, NAME_Select, NAME_Fire, NAME_AltFire, NAME_Hold, NAME_AltHold, NAME_Flash, NAME_AltFlash, NAME_None }; static ENamedName pickupstates[] = { NAME_Pickup, NAME_Drop, NAME_Use, NAME_None }; TMap checked; CheckStateLabels(obj, actorstates, SUF_ACTOR, "actor sprites"); if (obj->IsDescendantOf(NAME_Weapon)) { CheckStateLabels(obj, weaponstates, SUF_WEAPON, "weapon sprites"); } else if (obj->IsDescendantOf(NAME_CustomInventory)) { CheckStateLabels(obj, pickupstates, SUF_ITEM, "CustomInventory state chain"); } for (unsigned i = 0; i < obj->GetStateCount(); i++) { auto state = obj->GetStates() + i; if (state->NextState && (state->UseFlags & state->NextState->UseFlags) != state->UseFlags) { GetStateSource(state).Message(MSG_ERROR, TEXTCOLOR_RED "State %s links to a state with incompatible restrictions.\n", FState::StaticGetStateName(state).GetChars()); } } } //========================================================================== // // LoadActors // // Called from FActor::StaticInit() // //========================================================================== void ParseScripts(); void ParseAllDecorate(); void SynthesizeFlagFields(); void LoadActors() { cycle_t timer; timer.Reset(); timer.Clock(); FScriptPosition::ResetErrorCounter(); InitThingdef(); FScriptPosition::StrictErrors = true; ParseScripts(); FScriptPosition::StrictErrors = false; ParseAllDecorate(); SynthesizeFlagFields(); FunctionBuildList.Build(); if (FScriptPosition::ErrorCounter > 0) { I_Error("%d errors while parsing DECORATE scripts", FScriptPosition::ErrorCounter); } FScriptPosition::ResetErrorCounter(); for (int i = PClassActor::AllActorClasses.Size() - 1; i >= 0; i--) { auto ti = PClassActor::AllActorClasses[i]; if (ti->Size == TentativeClass) { if (ti->bOptional) { Printf(TEXTCOLOR_ORANGE "Class %s referenced but not defined\n", ti->TypeName.GetChars()); FScriptPosition::WarnCounter++; // the class must be rendered harmless so that it won't cause problems. ti->ParentClass = RUNTIME_CLASS(AActor); ti->Size = sizeof(AActor); } else { Printf(TEXTCOLOR_RED "Class %s referenced but not defined\n", ti->TypeName.GetChars()); FScriptPosition::ErrorCounter++; } continue; } if (GetDefaultByType(ti) == nullptr) { Printf(TEXTCOLOR_RED "No ActorInfo defined for class '%s'\n", ti->TypeName.GetChars()); FScriptPosition::ErrorCounter++; continue; } CheckStates(ti); if (ti->bDecorateClass && ti->IsDescendantOf(NAME_StateProvider)) { // either a DECORATE based weapon or CustomInventory. // These are subject to relaxed rules for user variables in states. // Although there is a runtime check for bogus states, let's do a quick analysis if any of the known entry points // hits an unsafe state. If we can find something here it can be handled wuth a compile error rather than a runtime error. CheckForUnsafeStates(ti); } // ensure that all actor bouncers have PASSMOBJ set. auto defaults = GetDefaultByType(ti); if (defaults->BounceFlags & (BOUNCE_Actors | BOUNCE_AllActors)) { // PASSMOBJ is irrelevant for normal missiles, but not for bouncers. defaults->flags2 |= MF2_PASSMOBJ; } } if (FScriptPosition::ErrorCounter > 0) { I_Error("%d errors during actor postprocessing", FScriptPosition::ErrorCounter); } timer.Unclock(); if (!batchrun) Printf("script parsing took %.2f ms\n", timer.TimeMS()); // Now we may call the scripted OnDestroy method. PClass::bVMOperational = true; StateSourceLines.Clear(); }