Replacing the Interactions in a Potion template (or changing the model in a Spell template) works, but animations seem to be tied to scripts, so instead of drinking the potion our hero just "casts" the Potion.
Any way around that or would this involve the use of the sdk to bring new scripts into the game?
Ergebnis 1 bis 12 von 12
-
- Registriert seit
- Mar 2009
- Beiträge
- 94
-
- Registriert seit
- May 2009
- Beiträge
- 1.688
Nope, as far as I know you can't set the executed actions while consuming a item and the effects it should have separately.
[Bild: dtc_sig.jpg]
Harald Iken: Überhaupt sollte man als Spieleentwickler das Wort "einfach" oder noch besser "mal eben" aus seinem Wortschatz streichen.
-
- Registriert seit
- Mar 2009
- Beiträge
- 94
So I finally found some time and motivation to look into the SDK.
Thanks to all the Scripts out there, I was able to get at least a basic grasp of the process.
I was just gonna intercept InventoryUse_Player_Sprint, push in InventoryUse_Player_ConsumePotion before that - but that obv did not work, since I dont wait between them.
(Is there a way to wait some time or wait for the Hero to end the Animation ?)
I also found some Code where a specific Animation is called ("Teleport"). I put that in there instead of ConsumePotion, it was not perfect since the animations overlapped, but whatever.
But where can I find a list of these Animation Strings?
https://hastebin.com/uherevasox.cpp
Hopefully someone with a little more knowledge than me can help me out a little bit.
-
- Registriert seit
- May 2009
- Beiträge
- 1.688
It's been a long time since I've worked on this stuff so take everything I say with a grain of salt.
1. In the context of AIFunctions you should probably avoid playing animations by yourself. Let the AI system do it, gCScriptProcessingUnit::sAIPlayAniInstr.
2. An AIFunction is a coroutine as far as I understand them. If you are not familiar with that term don't worry I'll explain it.
An AIFunction isn't supposed to actually execute anything, instead it should post commands to the AI system, then wait for them to get done and post the next command until you are satisfied. This is because of the problem you described, you don't know when the animation has finished. But if you let the engine handle this stuff, it will notify you as soon as it's done.
The return code of functions like gCScriptProcessingUnit::sAIPlayAniInstr tells you whether the engine needs some time to execute the instruction. If it does need some time, the AIFunction should pass control back to the engine and tell the engine that it should be called again when the instruction has been completed. That also means your AIFunction has to know where it left of last time. That's done through SRTSSStack and the macros FIRST_SUBTASK, NEXT_SUBTASK and SET_SUBTASK_DONE. Don't worry about how that actually works, I wrote it and I barely understood it back than. It doesn't matter when you are just using it.
3. It's probably a bad idea to actually hook an AIFunction. You should register the old AIFunction under a new name and a new AIFunction under the old name. The new AIFunction does the additional stuff you want and then passes controll on to the old AIFunction using the engine.
This is a more or less complex example of an AIFunction I wrote for dtc to show how the coroutine stuff works:
Code:#include "Interact_Player_dtc_RopeRoll.h" #include "../hacks.h" #include "../ScriptAIUtil.h" #include "../SPU_AIInstr_structs.h" GEBool GE_STDCALL Interact_Player_dtc_RopeRoll::Impl( bTObjStack<gScriptRunTimeSingleState>& SRTSSStack, gCScriptProcessingUnit* SPU) { Entity Self; Entity Interactor; GELPByte __FIXME_0014 = reinterpret_cast<GELPByte>(SRTSSStack[SRTSSStack.GetCount() - 1].__FIXME_0014); if(__FIXME_0014) { Self.AttachTo( reinterpret_cast<eCEntityProxy*>(__FIXME_0014 + 0x04)->GetEntity()); Interactor.AttachTo(reinterpret_cast<eCEntityProxy*>(__FIXME_0014 + 0x10)->GetEntity()); } FIRST_SUBTASK { SET_SUBTASK_DONE; gui2.SetPageMode(gEPageMode_Panorama, GEFalse); EffectSystem::StartImageEffect("OverlayBlackScreen", 0.0, 1.0, 1.0); //Hero_Stand_None_None_P0_Kneel_Begin_N_Fwd_00_%_00_P0_0 bCString Ani = Interactor.GetAni(g_strAction_Kneel, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0, gEAniState_Count); Interactor.AccessPropertySet<PSRoutine>().SetAniState(gEAniState_Kneel, gEAniState_Dummy0); CAIPlayAniInstr AIPlayAniInstr(Interactor, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //Hero_Kneel_None_None_P0_LockPick_Begin_N_Fwd_00_%_00_P0_0 bCString Ani = Interactor.GetAni(g_strAction_Lockpick, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0, gEAniState_Count); CAIPlayAniInstr AIPlayAniInstr(Interactor, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //Hero_Kneel_None_None_P0_LockPick_Loop_N_Fwd_00_%_00_P0_0 bCString Ani = Interactor.GetAni(g_strAction_Lockpick, g_strPhase_Loop, gEDirection_Fwd, GEFalse, gECombatPose_P0, gEAniState_Count); Interactor.StartPlayAni(Ani, -1, GEFalse, 1.0f, GETrue, static_cast<eAnimShared::eEMotionType>(0)); CAIWaitInstr AIWaitInstr(Interactor, 2000); if(!gCScriptProcessingUnit::sAIWaitInstr(&AIWaitInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; EffectSystem::StopImageEffect(); world.SetSectorStatus("RopeRoll", GEFalse); world.SetSectorStatus("RopeTeleporter", GETrue); //Hero_Kneel_None_None_P0_LockPick_End_N_Fwd_00_%_00_P0_0 bCString Ani = Interactor.GetAni(g_strAction_Lockpick, g_strPhase_End, gEDirection_Fwd, GEFalse, gECombatPose_P0, gEAniState_Count); CAIPlayAniInstr AIPlayAniInstr(Interactor, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //Hero_Kneel_None_None_P0_Stand_Begin_N_Fwd_00_%_00_P0_0 bCString Ani = Interactor.GetAni(g_strAction_Stand, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0, gEAniState_Count); Interactor.AccessPropertySet<PSRoutine>().SetAniState(gEAniState_Stand, gEAniState_Dummy0); CAIPlayAniInstr AIPlayAniInstr(Interactor, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; gui2.RestoreUserPageMode(); Entity::SetCameraTarget(Interactor, Interactor, GEFalse, *Hacks::GetUserCameraOffset(), GEFalse, None, -1.0); Entity::SetCurrentCameraByName("cam_normal", GETrue); Entity::GetPlayer().AccessPropertySet<PSRoutine>().ContinueRoutine(); } return GETrue; }
and this one shows how one AIFunction passes controll onto another:
Code:#include "PreInteract_Player_dtc_Goto.h" #include "../hacks.h" #include "../ScriptAIUtil.h" #include "../SPU_AIInstr_structs.h" namespace ScriptProxies { //note that Script_Game doesn't share AIFunction Proxies as far as i can tell. //i don't know if sharing them will cause problems gCScriptProxyAIFunction PreInteract_Player_Goto("PreInteract_Player_Goto"); } GEBool GE_STDCALL PreInteract_Player_dtc_Goto::Impl( bTObjStack<gScriptRunTimeSingleState>& SRTSSStack, gCScriptProcessingUnit* SPU) { Entity Interact; Entity User; gSAIScriptArgs* AIScriptArgs = reinterpret_cast<gSAIScriptArgs*>(SRTSSStack[SRTSSStack.GetCount() - 1].__FIXME_0014); if(AIScriptArgs) { Interact.AttachTo(AIScriptArgs->m_Other.GetEntity()); User.AttachTo(AIScriptArgs->m_Self.GetEntity()); } FIRST_SUBTASK { SET_SUBTASK_DONE; User.AccessPropertySet<PSRoutine>().SetLocalCallback("OnPlayerInteract_Cancel"); gui2.SetPageMode(gEPageMode_UserMin, GEFalse); Entity::SetCameraTarget(Interact, User, GEFalse, bCVector(0, 100, 0), GEFalse, None, -1.0); Entity::SetCurrentCameraByName("cam_dtc_interact_fix", GEFalse); AIScriptArgs = GE_NEW(gSAIScriptArgs(User, Interact)); return SPU->CallAIFunction(ScriptProxies::PreInteract_Player_Goto, AIScriptArgs); } return GETrue; }
All this has to be done so we can these SUBTASK thingys without completely fucking up the SRTSSStack.
There are certainly shorter and maybe even "easier" ways of doing all of this, but this is as far as I can tell the cleanest and correct one.[Bild: dtc_sig.jpg]
Harald Iken: Überhaupt sollte man als Spieleentwickler das Wort "einfach" oder noch besser "mal eben" aus seinem Wortschatz streichen.Geändert von Kuchenschlachter (18.03.2017 um 10:45 Uhr)
-
- Registriert seit
- Mar 2009
- Beiträge
- 94
Thanks a lot.
I have run into another problem though.
The ConsumePotion animation does not play - that seems to be a problem for a few animations that require an item in the heroes hands to interact with.
Other Animations such as Lockpick work though. So I tried getting the animation with GetHoldAni (not sure I used that right, but it certainly does not work).
Any suggestions?
Also is there an easy or prefered way to Debug stuff or at least write to the console?
Code:#include "Script.h" #include "ScriptPolicies.h" #include "ScriptDLLInterface.h" #include "SPU_AIInstr_structs.h" #include "ScriptAIUtil.h" namespace ScriptProxies { static gCScriptProxyAIFunction InventoryUse_Player_Sprint("InventoryUse_Player_Sprint"); } struct InventoryUse_Player_ConsumePotion_Sprint { static GEBool GE_STDCALL Impl( bTObjStack<gScriptRunTimeSingleState>& SRTSSStack, gCScriptProcessingUnit* SPU) { Entity Other; Entity Self; gSAIScriptArgs* AIScriptArgs = reinterpret_cast<gSAIScriptArgs*>(SRTSSStack[SRTSSStack.GetCount() - 1].__FIXME_0014); if(AIScriptArgs) { Other.AttachTo(AIScriptArgs->m_Other.GetEntity()); Self.AttachTo(AIScriptArgs->m_Self.GetEntity()); } FIRST_SUBTASK { SET_SUBTASK_DONE; //Consume Potion //Hero_Stand_None_None_P0_ConsumePotion_Begin_O_Fwd_00_%_00_P0_0 bCString Ani = Self.GetHoldAni(g_strAction_ConsumePotion, gEItemHoldType_Potion, gEItemHoldType_Potion, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0); CAIPlayAniInstr AIPlayAniInstr(Self, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //Consume Potion //Hero_Stand_None_None_P0_ConsumePotion_End_O_Fwd_00_%_00_P0_0 bCString Ani = Self.GetHoldAni(g_strAction_ConsumePotion, gEItemHoldType_Potion, gEItemHoldType_Potion, g_strPhase_End, gEDirection_Fwd, GEFalse, gECombatPose_P0); CAIPlayAniInstr AIPlayAniInstr(Self, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //InventoryUse_Player_Sprint AIScriptArgs = GE_NEW(gSAIScriptArgs(Self, Other)); return SPU->CallAIFunction(ScriptProxies::InventoryUse_Player_Sprint, AIScriptArgs); } return GETrue; } }; void GE_STDCALL ScriptInitImplementation() { bTObjArray<bCString> DLLNames; DLLNames.Add("Script_Game.dll"); AssertDLLsAreLoaded(DLLNames); using namespace ScriptPolicies; MANAGED_REGISTER(EScriptType_ScriptAIFunction, "InventoryUse_Player_ConsumePotion_Sprint", ClearScriptAIFunction<InventoryUse_Player_ConsumePotion_Sprint>); }
-
- Registriert seit
- May 2009
- Beiträge
- 1.688
That's quiet surprising to me. I was sure that there is no intrinstic requirement for any sort of additional entity to play the animations of an actor.
We usually use something like this and a tool like DebugView for logging.
Code:void DbgOut(GELPCChar p_c_Format, ...) { va_list p_c_ArgList; va_start(p_c_ArgList, p_c_Format); GEChar a_c_Buffer[255]; vsprintf_s(a_c_Buffer, 255, p_c_Format, p_c_ArgList); OutputDebugString(a_c_Buffer); va_end(p_c_ArgList); }
And you asked if there is a list of all animations, you can extract the the animations archives data/compiled/animations.p*.
Maybe the most sensible approach would be instead of playing any animation just invoke InventoryUse_Player_ConsumePotion or _AI_Consume before InventoryUse_Player_Sprint. Allthough this might cause serious problems, as InventoryUse_Player_Sprint wants to consume the item, it is invoked on and InventoryUse_Player_ConsumePotion does the same. Consuming an item twice... well that might not be such a good thing.
On the other hand I had a quick look at InventoryUse_Player_ConsumePotion and _AI_Consume, and there happens a hell of a lot of stuff to play all the necessary animations, get the rigth items into the actors hands and so on. InventoryUse_Player_Sprint is comparatively simple.
By the way I assume you use all of this with a newly created item, does that item have a PSCastInfo? Because as far as I can tell InventoryUse_Player_Sprint should crash for any item that doesn't have one.[Bild: dtc_sig.jpg]
Harald Iken: Überhaupt sollte man als Spieleentwickler das Wort "einfach" oder noch besser "mal eben" aus seinem Wortschatz streichen.
-
- Registriert seit
- Mar 2009
- Beiträge
- 94
Thanks, noted.
Will play around with that tomorrow.
Yeah, I already found that, but it wasn't terribly helpful.
I did search the SDK though and found a list of animation strings in Script\gs_psroutine.h.
That was kind of the original plan, but that did not really work because I could not wait on the first AIFunction to finish. But I might have done smth wrong then (in regards to Subtask and only hooking the function).
Will try that again and report back tomorrow.
Well I don't know ... I think through all my tests the item I used was often times not consumed at all. (but maybe I just did not properly pass that to the next function...)
How would one look at that?
I just copied a health potion, removed the health modifier, renamed it and put my InventoryUse_Player_ConsumePotion_Sprint in the Interactions.
Just checked with tple and no, it does not actually have a PSCastInfo - the way I call InventoryUse_Player_ConsumePotion_Sprint inside does not seem to care to much about the item anyways since it does not consume it either.
Thanks for being so helpful btw .
Did not expect such thorough, nice and fast answers .
-
- Registriert seit
- May 2009
- Beiträge
- 1.688
The animations played during _AI_Consume are...
Entity:etOverlayAni(g_strAction_ConsumePotion, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0)
Entity:etOverlayAni(g_strAction_ConsumePotion, g_strPhase_End, gEDirection_Fwd, GEFalse, gECombatPose_P0)
Ahhh... you are probably passing a gSAIScriptArgs to InventoryUse_Player_ConsumePotion without setting m_iParameter, right?
I guess InventoryUse_Player_ConsumePotion can't figure out which item it is supposed to process. Set it to the value of the AIScriptArgs.m_iParameter passed to you
(gSAIScriptArgs* AIScriptArgs = reinterpret_cast<gSAIScriptArgs*>(SRTSSStack[SRTSSStack.GetCount() - 1].__FIXME_0014).
That might actually do the trick. It's basically the index of the item in the players inventory.
I also had a second glance at InventoryUse_Player_Sprint. It can be and is much more lenient on "wrong" and incomplete parameters. Since it does basically nothing with the item except for calling PSCastInfo::Consume, which itself is protected against calls for items that don't have a PSCastInfo.
That explains why your Item isn't consumed.
"How do you look at that?"
Well... disassembler, I use IDA and everyone I've ever talked to does so too.
Well, you know, this is actually a very interesting topic.[Bild: dtc_sig.jpg]
Harald Iken: Überhaupt sollte man als Spieleentwickler das Wort "einfach" oder noch besser "mal eben" aus seinem Wortschatz streichen.
-
- Registriert seit
- Mar 2009
- Beiträge
- 94
Thanks .
Should have really tried all of the options :C.
That did the trick - combined with a PSCastInfo where ConsumeItem is set to true, the item gets properly consumed.
I did remove the PSCastInfo later on though (i now just consume the item in the code manually), since it would display a ManaCost which is a little dumb for a potion.
Wasn't yet very motivated to anything in that direction, but maybe I will try next week .
So here is what I got. Just putting the item in the heroes hands seems to be good enough for the animations.
And it already works pretty well - Only thing I already found is that the hero does not go back to combat automatically if you use the potion with your weapon out.
Code:#include "Script.h" #include "ScriptPolicies.h" #include "ScriptDLLInterface.h" #include "SPU_AIInstr_structs.h" #include "ScriptAIUtil.h" namespace ScriptProxies { static gCScriptProxyAIFunction InventoryUse_Player_Sprint("InventoryUse_Player_Sprint"); } struct InventoryUse_Player_ConsumePotion_Sprint { static GEBool GE_STDCALL Impl( bTObjStack<gScriptRunTimeSingleState>& SRTSSStack, gCScriptProcessingUnit* SPU) { Entity Other; Entity Self; GEInt Parameter; PSInventory& Inventory = Self.AccessPropertySet<PSInventory>(); //This is probably unnecessary GEInt OldStack = Inventory.GetRightHoldStack(); gECombatMode combat = Self.GetRestorePlayerCombatMode(); // get Arguments gSAIScriptArgs* AIScriptArgs = reinterpret_cast<gSAIScriptArgs*>(SRTSSStack[SRTSSStack.GetCount() - 1].__FIXME_0014); if(AIScriptArgs) { Other.AttachTo(AIScriptArgs->m_Other.GetEntity()); Self.AttachTo(AIScriptArgs->m_Self.GetEntity()); Parameter = AIScriptArgs->m_iParameter; } //even if this worked, it would not conform to the mechanics of every other Spell (i think) /* if(Self.GetMovementMode() == gECharMovementMode_Sprint) return GEFalse; */ FIRST_SUBTASK { SET_SUBTASK_DONE; //Put the Potion in the Heroes right hand Inventory.HoldRightStack(Parameter); //Consume Potion Animation //Hero_Stand_None_None_P0_ConsumePotion_Begin_O_Fwd_00_%_00_P0_0 bCString Ani = Self.GetOverlayAni(g_strAction_ConsumePotion, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0); //bCString Ani = Self.GetOverlayAni(g_strAction_ConsumePotion, gEItemHoldType_Potion, gEItemHoldType_Potion, g_strPhase_Begin, gEDirection_Fwd, GEFalse, gECombatPose_P0); CAIPlayAniInstr AIPlayAniInstr(Self, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //Consume Potion Animation //Hero_Stand_None_None_P0_ConsumePotion_End_O_Fwd_00_%_00_P0_0 bCString Ani = Self.GetOverlayAni(g_strAction_ConsumePotion, g_strPhase_End, gEDirection_Fwd, GEFalse, gECombatPose_P0); //bCString Ani = Self.GetOverlayAni(g_strAction_ConsumePotion, gEItemHoldType_Potion, gEItemHoldType_Potion, g_strPhase_End, gEDirection_Fwd, GEFalse, gECombatPose_P0); CAIPlayAniInstr AIPlayAniInstr(Self, 0, Ani, 0, GEFalse, 1.0, GEFalse, GEFalse, GEFalse); if(!gCScriptProcessingUnit::sAIPlayAniInstr(&AIPlayAniInstr, SPU, GEFalse)) return GEFalse; } NEXT_SUBTASK { SET_SUBTASK_DONE; //Not really necessary, but looks a little nicer Inventory.HoldRightStack(OldStack); //InventoryUse_Player_Sprint AIScriptArgs = GE_NEW(gSAIScriptArgs(Self, Other));//Dont need the Item here, It does not have PSCastInfo anyways (since that would show manacost lol) GEBool Result = SPU->CallAIFunction(ScriptProxies::InventoryUse_Player_Sprint, AIScriptArgs); //Remove the Item from the Inventory Inventory.ConsumeItems(Parameter, 1); //definitely does not do what I thought I would Self.SetRestorePlayerCombatMode(combat); return Result; } return GETrue; } }; void GE_STDCALL ScriptInitImplementation() { bTObjArray<bCString> DLLNames; DLLNames.Add("Script_Game.dll"); AssertDLLsAreLoaded(DLLNames); using namespace ScriptPolicies; MANAGED_REGISTER(EScriptType_ScriptAIFunction, "InventoryUse_Player_ConsumePotion_Sprint", ClearScriptAIFunction<InventoryUse_Player_ConsumePotion_Sprint>); }
-
- Registriert seit
- May 2009
- Beiträge
- 1.688
That's curious, when using an item from inventory weapons are sheathed, that's normal. Using an item through QuickUse the combat mode is restored automatically. This doesn't depend on your new AIFunction. QuickUse is implemented by the QuickUse_Player* scripts. QuickUse_Player just executes the items InventoryUse interaction and afterwards restores the combatmode by calling the script ResumePlayerTask. You shouldn't have to do anything for this to work correctly.
Get-/SetRestorePlayerCombatMode does virtually nothing, it just sets a field that is used in ResumePlayerTask to actually restore the combat mode.
Even if SetRestorePlayerCombatMode did what you thought it does you should call it in a separate SUBTASK. CallAIFunction probably returns imediatly, a long time before InventoryUse_Player_Sprint is actually done. Therefore your cleanup happens way too soon.
I did try to figure out how the combat mode is restored for InventoryUse_Player_ConsumePotion, and I suspected it to be related to _AI_HoldInventoryItems but after some superficial investigation it could as easily be InventoryUse_Player_ConsumePotion itself. It checks whether the UseType of the currently held item is a weapon, sets some flag and then... well that's where the superficial investigation would have become an in depth analysis...[Bild: dtc_sig.jpg]
Harald Iken: Überhaupt sollte man als Spieleentwickler das Wort "einfach" oder noch besser "mal eben" aus seinem Wortschatz streichen.
-
- Registriert seit
- Mar 2009
- Beiträge
- 94
Yes, you are right.
Apparently putting smth else in our heroes hand prevented that from happening.
As you can see I tried restoring whatever he was holding already (see OldStack), but that didn't work at all.(obviously in hindsight) (local variable definition, setting/using in different subroutines)
I just made it global and it seems to be working flawlessly now.
Thanks
-
- Registriert seit
- May 2009
- Beiträge
- 1.688
We're morons. Well, at least I am
I'm glad it finally works.[Bild: dtc_sig.jpg]
Harald Iken: Überhaupt sollte man als Spieleentwickler das Wort "einfach" oder noch besser "mal eben" aus seinem Wortschatz streichen.