InputHandler - Eingabeverarbeitung leicht gemacht

Übersicht BlitzMax, BlitzMax NG Codearchiv & Module

Neue Antwort erstellen

 

n-Halbleiter

Betreff: InputHandler - Eingabeverarbeitung leicht gemacht

BeitragSa, Mai 28, 2011 3:39
Antworten mit Zitat
Benutzer-Profile anzeigen
Guten Morgen Forum!

Ich möchte hier einen der Codes präsentieren, die ich in letzter Zeit so produziert habe: InputHandler.

Was sind InputHandler?
InputHandler sind eine Möglichkeit, Nutzereingaben zu verarbeiten. Und zwar Nutzereingaben jeder Art. Sie funktionieren mit den Funktionen aus BRL.PolledInput und - wenn ihr ähnliche Funktionen habt (für bspw. Geräte anderer Art) - auch mit anderem.

Wie nutze ich sie?
InputHandler werden genutzt, indem eine Instanz des Types TUserInputHandler (ein Beispiel gibt es weiter unten) über den Konstruktor erstellt wird. Dabei müssen einige Informationen übergeben werden (diese werden im Beispielcode erläutert).

Was für Vorteile bieten sie mir gegenüber den Standardfunktionen?
InputHandler bieten zwei Modi: Den Request Mode und den Event Mode. Der Request Mode ist der Modus, der aus dem Standardmodul (BRL.PolledInput) bekannt ist: Man fragt die Tastendrücke über bestimmte Funktionen ab. Zusätzlich zu den beiden bekannten Funktionen existiert noch die Möglichkeit, abzufragen, ob eine Taste losgelassen wurde.
Der Event Mode hingegen bietet alle Möglichkeiten, die man auch im Request Mode hat, und noch mehr: Man kann einem Tastendruck (oder dem Halten (für eine bestimmte Zeit)/Loslassen einer Taste) eine bestimmte Funktion zuweisen, die dann aufgerufen wird, wenn die Taste gedrückt/(eine bestimmte Zeit lang)gehalten/losgelassen wird. Dann gibt es noch die Listenerfunktion, welche für jeden von einer anderen Funktion unbearbeiteten (oder weitergereichten) Tastendruck aufgerufen wird. Als letztes gibt es noch SequenceHandler. Diese bieten die Möglichkeit, eine Funktion aufrufen zu lassen, wenn der Nutzer eine bestimmte Tastenfolge gedrückt hat (wer mal GTA gespielt hat: Das sieht dann so aus wie Cheats in den Spielen).
Wenn diese Features nicht für InputHandler sprechen, weiß ich auch nicht mehr weiter. Wink


Und nun: Die Codes!

TUserInputHandler.bmx BlitzMax: [AUSKLAPPEN]
SuperStrict

Import BRL.Blitz

Rem
Constants used.
End Rem

Const MODE_REQUEST:Byte=$0
Const MODE_EVENT:Byte=$1

'Flags for AddEventHandler
Const TRIGGER_HIT:Int=0
Const TRIGGER_DOWN:Int=1
Const TRIGGER_RELEASE:Int=2
Const TRIGGER_HOLD:Int=32
Const TIME_S:Int=4
Const TIME_MS:Int=8
Const RESET_AFTER:Int=16

Rem
bbdoc: Input Handler type
about: This type provides an easy-to-use interface for interaction with the user (about e.g. a keyboard). Just create an instance <br>
of this type with the constructor, pass it all the functions and information you want it to use and you__COMMENT2__
To have your Input Handlers work properly, you have to call the Update-Method in your mainloop. There are two modes to work <br>
with Input Handlers: Request mode and Event mode. Request mode works like you know it from BRL.PolledInput, except there are <br>
some more features possible: Triggering on release of keys and a function which returns the modifiers (e.g. Ctrl, Alt, Shift). <br>
In Event mode, you can use all the features of Request mode and more: There is the possibility of a Listener Function which is <br>
called for every generated event (Keyhits, holding down of a key, release of a key). The function is passed the corresponding <br>
event as a parameter so you can access all the information easily. In addition you can bind an event processing function <br>
to a specific action (pressing, holding or releasing of a key) of a specific key. This function becomes executes whenever <br>
the event happens, also with the event as a paremeter. Also, another nice feature of Event mode are sequences: Everytime <br>
a sequence of states (defined by you) becomes typed without interruption (with hitting a key as trigger), the sequence <br>
handler function becomes executed.
End Rem

Type TUserInputHandler

Const C_cHoldFlags:Int=4
Const C_iHoldFlags:Int=0
Const C_iHoldTime:Int=1
Const C_iHoldStarttime:Int=2
Const C_iHoldfActive:Int=3

Field _sMode:Byte

Field _ListenerFnc:Int(Event:TInputEvent)
Field _rgEventHandler:Int(Event:TInputEvent)[]
Field _rgDownEventHandler:Int(Event:TInputEvent)[]
Field _rgReleaseEventHandler:Int(Event:TInputEvent)[]

Field _rgHoldHandler:Int(Event:TInputEvent)[]
Field _rgHoldHandlerFlag:Int[,]

Field _LstEvents:TInputEvent

Field _LstSequences:TInputSequence
Field _LstQueuedEvents:TInputEvent
Field _QueuedLstTail:TInputEvent
Field _cQueuedEvents:Int

Field _cStates:Int
Field _StateFnc:Int(i:Int)
Field _rgState:Int[]

Field _cModifiers:Int
Field _ModifierFnc:Byte Ptr[]
Field _rgcModifierIndex:Int[]
Field _rgModifier:Int[][]

Field _StateDownFnc:Int(i:Int)
Field _rgStateDown:Int[]
Field _rgLastStateDown:Int[]

Method New()
Self._sMode=MODE_REQUEST
End Method

Rem
bbdoc: Function to create an Input handler
about: To create a proper working input handler you have to pass the amount of states, the maximum index as a paremeter the modifier <br>
functions have (0 for no parameter), the functions which return the states (Pressed, held down) and the function(s) which <br>
return(s) the modifiers. This function(s) have to be encapsulated in an array.
End Rem

Function Create:TUserInputHandler(cStates:Int,cModifierIndex:Int[],StateFnc:Int(i:Int),StateDownFnc:Int(i:Int),ModifierFnc:Byte Ptr[])
Return New TUserInputHandler.Init(cStates,cModifierIndex,StateFnc,StateDownFnc,ModifierFnc)
End Function
Method Init:TUserInputHandler(cStates:Int,cModifierIndex:Int[],StateFnc:Int(i:Int),StateDownFnc:Int(i:Int),ModifierFnc:Byte Ptr[])
Self._cStates=cStates
Self._StateFnc=StateFnc
Self._StateDownFnc=StateDownFnc

Self._rgState=New Int[cStates]
Self._rgStateDown=New Int[cStates]
Self._rgLastStateDown=New Int[cStates]
Self._rgEventHandler=Self._rgEventHandler[..cStates]
Self._rgDownEventHandler=Self._rgDownEventHandler[..cStates]
Self._rgReleaseEventHandler=Self._rgReleaseEventHandler[..cStates]

Self._rgHoldHandler=Self._rgHoldHandler[..cStates]
Self._rgHoldHandlerFlag=New Int[cStates,C_cHoldFlags]

If cModifierIndex<>Null
Self._cModifiers=cModifierIndex.length
Self._rgcModifierIndex=cModifierIndex
Self._ModifierFnc=ModifierFnc
Self._rgModifier=Self._rgModifier[..Self._cModifiers]
For Local i:Int=0 Until Self._cModifiers
Local c:Int=Max(1,Self._rgcModifierIndex[i])
Self._rgModifier[i]=New Int[c]
Next
Else
Self._cModifiers=0
End If

Return Self
End Method

Rem
bbdoc: Method to set mode
about: The mode sets which functions are
End Rem

Method SetMode(sMode:Int)
Self._sMode=sMode
End Method

Rem
bbdoc: Add an event handler to a specific state
End Rem

Method AddEventHandler(iState:Int,HandlerFnc:Int(Event:TInputEvent),Flag:Int=TRIGGER_HIT,Info:Int=0)
If iState<Self._rgState.length
If Flag=TRIGGER_HIT
Self._rgEventHandler[iState]=HandlerFnc
Else If Flag=TRIGGER_DOWN
Self._rgDownEventHandler[iState]=HandlerFnc
Else If Flag=TRIGGER_RELEASE
Self._rgReleaseEventHandler[iState]=HandlerFnc
Else
If Flag&TRIGGER_HOLD<>0
Local Time:Int=Info*(((Flag&TIME_S)<>0)*1000)+Info*((Flag&TIME_MS)<>0)
If Time<>0
Self._rgHoldHandler[iState]=HandlerFnc
Self._rgHoldHandlerFlag[iState,C_iHoldTime]=Time
Self._rgHoldHandlerFlag[iState,C_iHoldFlags]=Flag~TRIGGER_HOLD
End If
End If
End If
End If
End Method
Rem
bbdoc: Remove an event handler binding from a specific state
End Rem

Method RemoveEventHandler(iState:Int,TriggerAction:Int=TRIGGER_HIT)
If iState<Self._rgState.length
Select TriggerAction
Case TRIGGER_HIT
Self._rgEventHandler[iState]=Null
Case TRIGGER_DOWN
Self._rgDownEventHandler[iState]=Null
Case TRIGGER_RELEASE
Self._rgReleaseEventHandler[iState]=Null
Case TRIGGER_HOLD
Self._rgHoldHandler[iState]=Null
End Select
End If
End Method
Rem
bbdoc: Set the listener function (the function which becomes executed if an event has no handler applied)
End Rem

Method SetListenerFunction(ListenerFnc:Int(Event:TInputEvent))
If Self._sMode=MODE_EVENT
Self._ListenerFnc=ListenerFnc
End If
End Method

Rem
bbdoc: Add a sequence handler function.
about: The ID is just for removing the sequence properly. The second parameter has to be an array of integer (the states <br>
which have to occure in order for the sequence to trigger) and the third parameter is the function which should be executed.
End Rem

Method AddSequenceHandler(ID:Int,rgState:Int[],Fnc:Int())
Local Sequence:TInputSequence=TInputSequence.Create(ID,rgState,Fnc)
Sequence._Succ=Self._LstSequences
Self._LstSequences=Sequence
End Method
Rem
bbdoc: Remove the sequence handler with the ID given as a parameter.
End Rem

Method RemoveSequenceHandler(ID:Int)
Local Sequence:TInputSequence=Self._LstSequences
Local LastSequence:TInputSequence
While Sequence<>Null
If Sequence._ID=ID
If LastSequence=Null
Self._LstSequences=Self._LstSequences._Succ
Else
LastSequence._Succ=Sequence._Succ
End If
Return
End If

LastSequence=Sequence
Sequence=Sequence._Succ
Wend
End Method

Rem
bbdoc: Returns whether the key has been pressed and how many times
End Rem

Method IsPressed:Int(i:Int)
Return Self._rgState[i]
End Method
Rem
bbdoc: Returns whether the key is pressed and held down
End Rem

Method IsDown:Int(i:Int)
Return Self._rgStateDown[i]
End Method
Rem
bbdoc: Returns whether the key was released
End Rem

Method IsReleased:Int(i:Int)
Return (Self._rgLastStateDown[i]-Self._rgStateDown[i])=1
End Method

Rem
bbdoc: Returns the modifier with index i, parameter ii (leave the there if the function has no parameters)
End Rem

Method GetModifier:Int(i:Int,ii:Int=0)
If i<Self._cModifiers
If ii<Self._rgModifier[i].length
Return Self._rgModifier[i][ii]
End If
End If
End Method

Rem
bbdoc: Updates all information stored in the InputHandler
End Rem

Method Update()
'Update states and modifiers
Local c:Int=Self._cModifiers
For Local i:Int=0 Until c
If Self._rgcModifierIndex[i]=0
Local Fnc:Int()=Self._ModifierFnc[i]
Self._rgModifier[i][0]=Fnc()
Else
Local Fnc:Int(i:Int)=Self._ModifierFnc[0]
For Local ii:Int=0 Until Self._rgcModifierIndex[i]
Self._rgModifier[i][ii]=Fnc(ii)
Next
End If
Next
c=Self._cStates
For Local i:Int=0 Until c
Self._rgState[i]=Self._StateFnc(i)
Self._rgLastStateDown[i]=Self._rgStateDown[i]
Self._rgStateDown[i]=Self._StateDownFnc(i)

'Emit events
If Self._sMode=MODE_EVENT
Local Trigger:Int,fThrow:Int
If Self.IsPressed(i)=True
fThrow=True
Trigger=TRIGGER_HIT
Else If Self.IsDown(i)=True
If Self._rgHoldHandlerFlag[i,C_iHoldfActive]=False
Self._rgHoldHandlerFlag[i,C_iHoldStarttime]=MilliSecs()
Self._rgHoldHandlerFlag[i,C_iHoldfActive]=True
End If
fThrow=True
Trigger=TRIGGER_DOWN
Else If Self.IsReleased(i)=True
Self._rgHoldHandlerFlag[i,C_iHoldfActive]=False
fThrow=True
Trigger=TRIGGER_RELEASE
End If
If fThrow
If Self._rgHoldHandlerFlag[i,C_iHoldfActive]=True
If MilliSecs()>=(Self._rgHoldHandlerFlag[i,C_iHoldStarttime]+Self._rgHoldHandlerFlag[i,C_iHoldTime])
Trigger=TRIGGER_HOLD
If (Self._rgHoldHandlerFlag[i,C_iHoldFlags]&RESET_AFTER)=RESET_AFTER
Self._rgHoldHandlerFlag[i,C_iHoldStarttime]=MilliSecs()
Else
Self._rgHoldHandlerFlag[i,C_iHoldfActive]=-1
End If
Else
fThrow=False
End If
End If
If fThrow
Local rgModifier:Int[][]
rgModifier=rgModifier[..Self._cModifiers]
For Local ii:Int=0 Until Self._cModifiers
Local c:Int=Self._rgcModifierIndex[ii]
rgModifier[ii]=New Int[c]
For Local iii:Int=0 Until c
rgModifier[iii]=Self._rgModifier[iii]
Next
Next
Local Event:TInputEvent=Self._LstEvents
Self._LstEvents=TInputEvent.Create(i,rgModifier,Trigger)
Self._LstEvents._Succ=Event
End If
End If
End If
Next

'Update events
If Self._sMode=MODE_EVENT
While Self._LstEvents<>Null
Local Event:TInputEvent=Self._LstEvents

'Event processing by sequence
If Event.Trigger()=TRIGGER_HIT
Local fQueue:Int=False
Local Sequence:TInputSequence=Self._LstSequences
While Sequence
If Self._cQueuedEvents<Sequence._cStates
If Sequence._rgState[Self._cQueuedEvents]=Event.State()
fQueue=True
Exit
End If
End If
Sequence=Sequence._Succ
Wend

If fQueue=True
Local fDeleteQueue:Int=False

Self._cQueuedEvents:+1
If Self._LstQueuedEvents=Null
Self._LstQueuedEvents=Event
Else
Self._QueuedLstTail._Succ=Event
End If
Self._QueuedLstTail=Event

Local Sequence:TInputSequence=Self._LstSequences
While Sequence
If Self._cQueuedEvents=Sequence._cStates
Local fMatch:Int=True
Local Event2:TInputEvent=Self._LstQueuedEvents
For Local i:Int=0 Until Self._cQueuedEvents
If Sequence._rgState[i]<>Event2.State()
fMatch=False
Exit
End If
Event2=Event2._Succ
Next
If fMatch=True
Sequence._Fnc()
fDeleteQueue=True
Exit
End If
End If
Sequence=Sequence._Succ
Wend
If fDeleteQueue
Self._cQueuedEvents=0
Self._QueuedLstTail=Null
Self._LstQueuedEvents=Null
End If
Else
Self._cQueuedEvents=0
Self._LstQueuedEvents=Null
Self._QueuedLstTail=Null
End If
End If

'Event processing by handler
Local rg:Int(Event:TInputEvent)[]
Select Event.Trigger()
Case TRIGGER_HIT
rg=Self._rgEventHandler
Case TRIGGER_DOWN
rg=Self._rgDownEventHandler
Case TRIGGER_RELEASE
rg=Self._rgReleaseEventHandler
Case TRIGGER_HOLD
rg=Self._rgHoldHandler
End Select
If rg[Event._State]<>Null
rg[Event._State](Event)
Else
Event.Pass()
End If

'Event processing by listener
If Event._fThrow=True And Self._ListenerFnc<>Null
Self._ListenerFnc(Event)
End If
Self._LstEvents=Self._LstEvents._Succ
Wend
End If
End Method

End Type

Rem
bbdoc: InputEvent type
End Rem

Type TInputEvent

Field _fThrow:Int
Field _Succ:TInputEvent
Field _State:Int
Field _rgModifiers:Int[][]
Field _Trigger:Int

Method New()
Self._fThrow=False
End Method

Function Create:TInputEvent(State:Int,rgModifiers:Int[][],Trigger:Int=TRIGGER_HIT)
Local InputEvent:TInputEvent=New TInputEvent

InputEvent._State=State
InputEvent._rgModifiers=rgModifiers
InputEvent._Trigger=Trigger

Return InputEvent
End Function

Rem
bbdoc: Let the Event be passed from the handling function to the listener
End Rem

Method Pass()
Self._fThrow=True
End Method

Rem
bbdoc: Returns the state of the event
End Rem

Method State:Int()
Return Self._State
End Method
Rem
bbdoc: Return the modifier which were present to the event__COMMENT9__
End Rem

Method Modifier:Int(i:Int,ii:Int=0)
If i<Self._rgModifiers.length
If ii<Self._rgModifiers[i].length
Return Self._rgModifiers[i][ii]
End If
End If
End Method
Rem
bbdoc: Return the trigger type of the event (0: TRIGGER_HIT, 1: TRIGGER_DOWN, 2: TRIGGER_RELEASE)
End Rem

Method Trigger:Int()
Return Self._Trigger
End Method

End Type

Private

Type TInputSequence

Field _rgState:Int[]
Field _cStates:Int
Field _ID:Int
Field _Succ:TInputSequence
Field _Fnc:Int()

Function Create:TInputSequence(ID:Int,rgState:Int[],Fnc:Int())
Local Sequence:TInputSequence=New TInputSequence

Sequence._ID=ID
Sequence._rgState=rgState
Sequence._cStates=rgState.length
Sequence._Fnc=Fnc

Return Sequence
End Function

End Type


Und ein Beispielcode, kommentiert, damit die ganze Sache verständlich wird. BlitzMax: [AUSKLAPPEN]
SuperStrict

Import "TUserInputHandler.bmx"

Graphics(800,600,0,0)
Global Timer:TTimer=CreateTimer(60)

Rem
Parameter für den Konstruktor: Anzahl der Stati; Ein Array von Integern, das zum Einen (durch seine Länge) die Anzahl der Funktionen
angibt, die Modifikatoren zurückgeben (beispielsweise Shift, Strg oder Alt), zum Anderen (durch die Einträge) die Anzahl der Modifikatoren
festlegt, die für jede Funktion zurückgegeben werden (0 bedeutet, dass die Funktion überhaupt keinen Parameter erwartet);
Die Funktion, die zurückgibt, ob ein bestimmter Status ausgelöst wurde (ob eine Taste gedrückt wurde); Die Funktion, die zurückgibt,
ob ein bestimmter Status gehalten wird (ob eine Taste gehalten wird); Ein Array an Funktionen (die alle zu "Byte Ptr" gecastet werden
müssen), die die Modifikatoren zurückgeben.
End Rem

Global KeyboardInput:TUserInputHandler=TUserInputHandler.Create(255,Null,KeyHit,KeyDown,Null)'Tastaturhandler erstellen
KeyboardInput.SetMode(MODE_EVENT)'Eventbasierten Modus aktivieren

KeyboardInput.AddEventHandler(KEY_SPACE,SpaceKeyHit)'Die Funktion SpaceKeyHit mit dem Status KEY_SPACE verknüpfen
KeyboardInput.AddEventHandler(KEY_LEFT,LeftKeyRelease,TRIGGER_RELEASE)'Die Funktion LeftKeyRelease mit dem Status KEY_LEFT und der
'Aktion des Loslassens des Status verknüpfen
KeyboardInput.AddEventHandler(KEY_RIGHT,RightKeyDown,TRIGGER_DOWN)'Ähnliches mit dem Halten der Taste KEY_RIGHT
KeyboardInput.SetListenerFunction(KeyboardListener)'Die Listenerfunktion als die Funktion KeyboardListener festlegen

KeyboardInput.AddEventHandler(KEY_D,KeyDHold,TRIGGER_HOLD|TIME_S,2)'Als Eventhandlerfunktion KeyDHold mit dem Drücken des Status KEY_D für
'2 Sekunden verknüpfen
KeyboardInput.AddEventHandler(KEY_S,KeySHold,TRIGGER_HOLD|TIME_MS|RESET_AFTER,1500)

KeyboardInput.AddSequenceHandler(0,[KEY_C,KEY_H,KEY_E,KEY_A,KEY_T],TypedCHEAT)'Die Funktion TypedCHEAT mit der Tastenfolge KEY_C,KEY_H,
'usw. verknüpfen und mit der ID 0 speichern.
KeyboardInput.AddSequenceHandler(1,[KEY_H,KEY_A,KEY_L,KEY_T],TypedHALT)'So ähnlich

Global MouseInput:TUserInputHandler=TUserInputHandler.Create(4,[0,0],MouseHit,MouseDown,[Byte Ptr(MouseX),Byte Ptr(MouseY)])'Maushandler erstellen

Repeat

'Über die Definition der Funktionen MouseX() bzw. MouseY() als Modifier des InputHandlers für Mausabfragen lassen sich diese
'Werte als Modifier des InputHandlers abrufen.
DrawText("Maus bei: "+MouseInput.GetModifier(0)+","+MouseInput.GetModifier(1),10,10)

'Mausabfragen auf dem üblichen Weg (nur, dass es durch die InputHandler noch eine weitere Möglichkeit der Abfrage gibt)
If MouseInput.IsPressed(MOUSE_LEFT)
DebugLog "Linke Maustaste gedrueckt."
Else If MouseInput.IsDown(MOUSE_MIDDLE)
DebugLog "Mittlere Maustaste gehalten."
Else If MouseInput.IsReleased(MOUSE_RIGHT)
DebugLog "Rechte Maustaste losgelassen."
End If

'Die InputHandler updaten (sonst funktionieren sie nicht Wink )
KeyboardInput.Update()
MouseInput.Update()

WaitTimer(Timer)
Flip 0
Cls

Forever
End

'Die Funktionen, die verknüpft wurden

'Die sog. SequenceHandler
Function TypedHALT()
DebugLog "Tastenfolge H A L T wurde gedrueckt."
End Function
Function TypedCHEAT()
DebugLog "Tastenfolge C H E A T wurde gedrueckt."
End Function

Rem
Die Listenerfunktion, die direkt zwei der Methoden eines InputEvents zeigt: Trigger() und State(). Trigger() gibt zurück, auf welchem
Weg das Event aktiviert wurde (Taste gedrückt (TRIGGER_HIT), Taste gehalten (TRIGGER_DOWN) oder Taste losgelassen (TRIGGER_RELEASE)),
State() gibt zurück, welche Taste ausgelöst wurde. Es existiert noch die Methode Modifier(Int,Int), die die Modifier zurückgibt, die
zur Erstellung des Events vorlagen (Erster Parameter: Welche Funktion, zweiter Parameter: Der Parameterwert der Funktion), und die
Methode Pass(), bei der das Event von der es direkt bearbeitenden Funktion weitergegeben wird an die Listenerfunktion.
End Rem

Function KeyboardListener:Int(Event:TInputEvent)
DebugLog "Taste gedrueckt (Code): "+Event.State()+", Trigger: "+Event.Trigger()
If Event.State()=KEY_ESCAPE Then End
End Function
'Die EventHandler
Function SpaceKeyHit:Int(Event:TInputEvent)
DebugLog "Space gedrueckt."

Event.Pass()
End Function
Function LeftKeyRelease:Int(Event:TInputEvent)
DebugLog "Linke Richtungstaste losgelassen."
End Function
Function RightKeyDown:Int(Event:TInputEvent)
DebugLog "Rechte Richtungstaste gehalten."
End Function
Function KeyDHold:Int(Event:TInputEvent)
DebugLog "Taste D fuer 2 Sekunden gehalten."
End Function
Function KeySHold:Int(Event:TInputEvent)
DebugLog "Taste S fuer 1,5 Sekunden gehalten."
End Function


Ich hoffe, diese Sache hilft jemandem. Über Feedback würde ich mich freuen. Smile

EDIT (19.06.2011): Kleine Erweiterung, die es ermöglicht, Eventbehandelnde Funktionen nach einer bestimmten Zeit des Haltens einer Taste aufrufen zu lassen.

Neue Antwort erstellen


Übersicht BlitzMax, BlitzMax NG Codearchiv & Module

Gehe zu:

Powered by phpBB © 2001 - 2006, phpBB Group