Jump to content

[C/C++|CLEO|SCM|SA|VC|3]Opcodes maken voor gebruik in CLEO scripts


Recommended Posts

Geplaatst: (bewerkt)

Opcodes maken voor gebruik in CLEO scripts

Inhoud

1. Inleiding

2. Benodigdheden

3. Voorbereidingen

4. Informatie omtrent CLEO opcodes

5. Project template maken

5.1. New project

5.2. Project properties

5.3. Default files

6. Cleo specifieke code

6.1. Registreren van een opcode

6.2. Communiceren met het script

6.3. Overige CLEO functies

7. Een opcode maken

7.1. Opcode declareren en registreren

7.2. Callback implementatie: parameters

8. Sannybuilder compiler

8.1. Opcodes.txt

8.2. SCM.ini

9. Gebruiken in een CLEO script

10. Testen

11. Tot slot

Inleiding

In deze tutorial ga ik uitleggen hoe je nieuwe opcodes kan maken voor in je CLEO scripts. Deze tutorial is gemaakt omdat er weinig informatie over te vinden is, en kennis moet je delen.Deze tutorial is gericht op mensen met kennis van programmeren en die van GTA modden houden :).

Benodigdheden

  • Grand theft auto 3, VC of SA.

In deze tutorial maak ik gebruik van San Andreas, echter moet dit werken voor bovengenoemde versies.
  • CLEO 4, hier te vinden: downloadlink, Nieuwe link: Download
  • CLEO 4 SDK, vink bij de installatie van CLEO 4 de CLEO 4.1 SDK aan.
  • Visual Studio

In deze tutorial maak ik gebruik van Microsoft Visual Studio Professional 2013, de Express variant voor C/C++ is
van de website van Microsoft. Ben je een student? Dan kan je via de Microsoft
de Professional-versie gratis downloaden. Voor deze tutorial voldoet de Express variant.
  • Sanny builder, hier te downloaden: downloadlink
  • Ervaring met programmeren, bij voorkeur C en/of C++.
  • Ervaring met de scripttaal SCM.

Voorbereidingen

Zorg ervoor dat je:

  • Een van bovenstaande GTA versies heb geïnstalleerd
  • CLEO 4 hebt geïnstalleerd
  • Sannybuilder hebt geïnstalleerd
  • Visual Studio hebt geïnstalleerd
  • CLEO 4 SDK geinstalleerd is.

Informatie omtrent CLEO opcodes

CLEO opcodes verschillen van de standaard opcodes die het spel zelf gebruikt. De standaard opcodes van het spel die kan je zowel vanuit SCM scripts en CLEO scripts aanroepen, CLEO opcodes daarentegen kan je enkel aanroepen wanneer je CLEO geïnstalleerd hebt, en deze dus beschikbaar is.CLEO opcodes die roepen een functie aan in een externe bibliotheek, deze externe bibliotheken kan je vinden in je CLEO folder, deze hebben de extensie .CLEO. De .CLEO bestanden zijn in feite gewone dll’s en bevatten de code die uitgevoerd wordt wanneer de desbetreffende opcode in een script aangeroepen wordt.

Project template maken

We gaan nu het project voor de opcode maken, hier komt de code te staan die bij je zelfgemaakte opcode hoort.

New project

Als eerste gaan we Visual Studio opstarten. Hierin gaan we naar: File > new > Project. In het linker deelvenster kies je: Templates > Visual C++ > Win32 Project. Vul onder een naam voor het project en voor de solution in.In deze tutorial hanteer ik de projectnaam: cleo_opcode_tutorial en de soluction naam: cleoOpcodes. Druk op Ok, een venster verschijnt, druk hier op next. Selecteer nu onder Application type: DLL. Onder Additional options vink je het vakje Empty project aan en druk op Finish.

Project properties

Voordat we met het project aan de slag gaan, moeten we eerst enkele zaken instellen. Druk met de rechtermuisknop op het project (dus niet de solution) en selecteer Properties. Ga nu naar onderstaande en pas aan:

  • Configuration Properties > General > Project Defaults > Character Set

Pas de Character Set aan naar: Not Set.
  • Configuration Properties > C/C++ > General > Additional Include Directories

Druk hier op edit (uitklappen eerst) Vul hier het pad in waar je de CLEO_SDK map hebt staan.
  • Configuration Properties > C/C++ > Advanced > Compile As

Kies hier of je een C of C++ plugin gaat maken. In dit deel van de tutorial wordt gebruikt gemaakt van C++.
  • Configuration Properties > Linker > General > Output file

Hier kan je aanpassen waar de DLL komt te staan na het compilen. Het is net zo makkelijk dat hij meteen de juiste extensie heeft. Het pad wat je er neer zet, kan relatief zijn aan je huidige projectmap. Ik kies ervoor om in de rootmap van de solution een map te maken die cleo_plugins heet. Dus in mijn geval staat er bij Output file: ..\cleo_plugins\$(ProjectName).cleo. Als je wilt, kan je er ook voor zorgen dat het meteen in je GTA SA CLEO map staat, aan jou de keus
:)
.
  • Configuration Properties > Linker > Input > Additional Dependencies

Druk hier op edit (uitklappen eerst) en voeg het volgende toe:\CLEO.lib
  • Configuration Properties > Linker > Manifest File > Generate Manifest

Verander deze waarde in: No (/MANIFEST:NO)

We zijn nu klaar met de project properties instellen.

Default files

Voor elk CLEO plugin project wat je maakt zijn er enkele bestanden vereist, ook is het handig is je gewoon elke keer dezelfde structuur gebruikt:

  • Header file: opcodes.h

#pragma once
BOOL InitOpcodes();

  • Header file: Stdafx.h

#pragma once

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>

  • Source file: Dllmain.cpp

#include "stdafx.h"
#include "opcodes.h"

BOOL APIENTRY DllMain(HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
  return InitOpcodes();
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
 break;
}
return TRUE;

}

  • Source file: opcodes.cpp

#include "stdafx.h"
#include "CLEO.h"

BOOL InitOpcodes()
{
BOOL result = FALSE;
return result;
}

Zoals je misschien al ziet in dllmain.cpp, wordt initOpcodes() in opcodes.cpp aangeroepen wanneer de dll succesvol aan het proces verbonden zit.In de initOpcodes kunnen we nu onze eigen opcode registreren.

CLEO Specifieke code

Voordat we onze eigen opcode kunnen registreren, is het van belang om te weten welke types we moeten gebruiken, en hoe een opcode geregistreerd wordt. Ook zou het reuze handig zijn als we weten hoe je code communiceert met de variabelen in je CLEO script.

Registreren van een opcode

Elke opcode heeft een indentifier. De indentifier voor eigengemaakte cleo opcodes heeft een bereik van 0AF0 t/m 7FFF.Elke opcode die je maakt moet een unieke indentifier hebben. Ook heeft elke opcode een callback nodig, de callback is de functie in de dll die bij je opcode hoort, deze declareren we op de volgende manier:

OpcodeResult WINAPI <callback naam> (CScriptThread* thread);

Nu we een callback hebben kunnen we de opcode indentifier hieraan koppelen, dit doen we met:

CLEO_RegisterOpcode(<opcode indentifier>, &<callback naam>)

Met bovenstaand is je opcode geregistreerd en kan je beginnen met je eigen code te schrijven.

Communiceren met het script

Een belangrijk, en onmisbaar onderdeel is het verkrijgen en verzenden van waardes van variabelen tussen je plugin en een CLEO script. Hiervoor zijn enkele CLEO specifieke functies:

  • CLEO_SetIntOpcodeParam(CScriptThread* thread, DWORD value)

Voor het doorgeven van een integer naar de eerst volgende parameter.
  • CLEO_SetFloatOpcodeParam(CScriptThread* thread, float value)

Voor het doorgeven van een float waarde naar de eerst volgende parameter.
  • CLEO_WriteStringOpcodeParam(CScriptThread* thread, LPCSTR str)

Voor het doorgeven van een string waarde naar de eerst volgende parameter.
  • CLEO_GetIntOpcodeParam(CScriptThread* thread)

Voor het lezen van een integer van de eerst volgende parameter.
  • CLEO_GetFloatOpcodeParam(CScriptThread* thread)

Voor het lezen van een float waarde van de eerst volgende parameter.
  • CLEO_ReadStringOpcodeParam(CScriptThread* thread, LPSTR buf, int size);

Voor het lezen van een tekenreeks vanaf de eerst volgende parameter.

Er is een mogelijkheid om alle variabelen die in huidig opcode gebruikt worden in een keer op te halen, dit kan tot maximaal 32 variabelen.

  • CLEO_RetrieveOpcodeParams(CScriptThread* thread, int count)

Hiermee worden het aantal opgegeven parameters van de huidige opcode gelezen en in de CLEO variabel opcodeParams gezet. Een voorbeeld om deze te gebruiken:

float x, y, z;
CLEO_RetrieveOpcodeParams(thread, 3);
x = opcodeParams[0].fParam; y = opcodeParams[1].fParam; z = opcodeParams[2].fParam;

  • CLEO_RecordOpcodeParams(CScriptThread* thread, int count)

Hiermee kan je waardes in de CLEO variabel opcodeParams zetten.

Overige CLEO functies

CLEO kent naast de register en lees/schrijf functies ook nog enkele andere, deze staan hieronder beschreven.

  • CLEO_GetVersion()

Hiermee kun je de huidige CLEO versie achterhalen.
  • CLEO_GetGameVersion()

Hiermee kan je de huidige GTA versie achterhalen, de waardes waarmee het vergeleken kan worden zijn:

- GV_US10: Amerikaanse versie 1.0;.

- GV_US11: Amerikaanse versie 1.01, deze wordt niet ondersteund door CLEO.

- GV_EU10: Europese versie 1.0.

- GV_EU11: Europese versie 1.01.

- GV_UNK: Onbekende versie
  • CLEO_GetOperandType(CScriptThread* thread);

Hiermee kan je bepalen wat voor type de waarde van de eerst volgende parameter is.

Mogelijke types zijn:
  • globalVar: Type: variabel (bijvoorbeeld $PLAYER_ACTOR)
  • localVar: Type: variabel (Bijvoorbeeld 0@)
  • globalArr: Type: array (Bijvoorbeeld: $Girl_Weapons (0@,12i) (zie main.scm))
  • localArr: Type: array (Bijvoorbeeld: 1@(0@,12i))
  • imm8: Type: Integer (8 bit)
  • imm16: Type: Integer (16 bit)
  • imm32: Type Integer (32 bit)
  • imm32f: Type: Float
  • vstring: Type: String (long) (Bijvoorbeeld :“test”)
  • sstring: Type: String (short) (Bijvoorbeeld: ‘TE1’)
  • globalVarVString: Type: String (long) (Bijvoorbeeld: v$globalVar)
  • localVarVString: Type: String (long) (Bijvoorbeeld: 0@v)
  • globalVarSString: Type: String (short) (Bijvoorbeeld: s$globalvar)
  • localVarSString: Type: String (short) (Bijvoorbeeld: 0@s)

  • CLEO_SetThreadCondResult(CScriptThread* thread, BOOL result)

Hiermee sluit je de aangeroepen functie van de opcode af en je geeft hier aan of het succesvol was of niet. Dit kan handig zijn als bevestiging als je verder geen waardes teruggeeft aan het script.
  • CLEO_SkipOpcodeParams(CScriptThread* thread, int count)

Met deze functie kan je het aangegeven aantal parameters overslaan.
  • CLEO_ThreadJumpAtLabelPtr(CScriptThread* thread, int labelPtr)

Met deze functie zal je CLEO script naar de aangegeven geheugenlocatie springen, om hier het script voort te zetten.

Een opcode maken

Met alle bovenstaande informatie kunnen we nu beginnen aan het maken van onze eigen opcode.Om het in deze tutorial kort en simpel te houden, zal ik een simpele rekensom doen en deze terugschrijven naar de parameter.Voordat we nu echt de opcode kunnen gaan declareren, moeten we eerst bedenken welke indentifier hij krijgt. Als indentifier kies ik voor de waarde: 0B30.

Opcode declareren en registeren

Wat ikzelf fijn vind om te doen is in opcodes.cpp na de includes de indentifiers van de variabelen neer te zetten:

#define OPCODE_TUTORIAL 0x0B30

Onder de opcode indentifiers is het makkelijk om de callback(s) te definiëren.

OpcodeResult WINAPI Script_Tutorial_opcode(CScriptThread* thread);

Nu de indentifiers en callback(s) bekend zijn, is het tijd om deze te registreren.

BOOL InitOpcodes()
{
BOOL result = FALSE;
if (CLEO_RegisterOpcode(OPCODE_TUTORIAL, &Script_Tutorial_opcode))
{
 result = TRUE;
}
return result;
}

Callback implementatie: parameters

Nu de opcode(s) geregistreerd zijn kunnen we in de callback van elke opcode alles doen wat we willen. Zet het volgende onder de functie InitOpcodes():

OpcodeResult WINAPI Script_Tutorial_opcode(CScriptThread* thread)
{
}

Nu moeten we gaan verzinnen wat voor parameters deze opcode heeft, welke parameters zijn om uit te lezen, en welke om naar toe te schrijven, en wat voor type is die parameter?Als voorbeeld ga ik een opcode maken waar je een getal opgeeft, deze wordt vermenigvuldigd met het tweede getal en het resultaat hiervan wordt weggeschreven naar het script. Nu moeten we gaan nadenken over de volgorde hoe we alles in willen gaan lezen.Ik wil dat mijn opcode straks op de volgende manier in een cleo script gebruikt wordt:

0@ = tutorial_opcode_1_vermenigvuldigd 1@ met 2@

Als we bovenstaande nemen dan weten we het resultaat van 0@ pas als we eerst 1@ en 2@ hebben ingelezen, dat is dus ook precies de volgorde hoe we dat gaan doen.Als we wiskundige berekeningen op een variabel willen loslaten, dan is het van belang dat we zeker weten dat de variabel geldig is. Laten we bovenstaande dus gaan implementeren:

OpcodeResult WINAPI Script_Tutorial_opcode(CScriptThread* thread)
{

BOOL result = FALSE;
int varA = 0, varB = 0, varC = 0;
//Lees 1e parameter uit
switch (CLEO_GetOperandType(thread))
{

case globalVar:
case localVar:
case globalArr:
case localArr:
case imm8:
case imm16:
case imm32:
 varA = CLEO_GetIntOpcodeParam(thread);
 result = TRUE;
  break;
default:
 CLEO_SkipOpcodeParams(thread, 1);
 result = FALSE;

}

//Lees 2e parameter uit
switch (CLEO_GetOperandType(thread))
{

case globalVar:
case localVar:
case globalArr:
case localArr:
case imm8:
case imm16:
case imm32:
 varB = CLEO_GetIntOpcodeParam(thread);
 result = TRUE;
 break;
default:
 CLEO_SkipOpcodeParams(thread, 1);
 result = FALSE;

}
//Kloppen beide?
if (result)
{
 varC = (varA * varB);
 CLEO_SetIntOpcodeParam(thread, varC); //Schrijf de waarde naar de 3e parameter
}
else
{
 CLEO_SetIntOpcodeParam(thread, -1); //Schrijf de waarde naar de 3e parameter
}
return OR_CONTINUE;

}

Bovenstaande code is inefficiënt (dubbele switch), maar het gaat erom dat het idee duidelijk is.Wanneer je bovenstaande hebt gevolgd, kan je nu je plugin compilen.Wanneer je plugin gecompiled is, moet je deze in de CLEO map van je GTA zetten.

Sannybuilder compiler

De volgorde van op welke manier de parameters ingelezen/geschreven worden wordt bepaald door de compiler van Sannybuilder. De compiler beslist dit aan de hand van de configuratiebestanden.

Navigeer naar de map waar je Sannybuilder geïnstalleerd hebt. In deze map vind je een map: data.In de map data staat voor elke GTA die Sannybuilder ondersteund een map. In mijn geval is het SanAndreas, de map sa dus.De bestanden die aangepast moeten worden zijn:

  • Opcodes.txt
  • SASCM.ini (voor GTA Vice City is het: VICESCM.ini, voor GTA 3 is het SCM.ini)

opcodes.txt

Het bestand opcodes.txt is simpelweg het bestand waarin de opcodes staan zoals je ze in een SCM/CLEO script zou gebruiken. Het formaat van een opcode in dit bestand is:

[opcode indentifier]: [tekst en parameters]

Ik heb besloten dat mijn opcode er op de volgende manier uit wilt laten zien:

0@ = tutorial_opcode_1_vermenigvuldig 1@ met 2@ 

Hier hoeven we enkel nog de indentifier voor te zetten met een dubbelepunt, het volgende komt dus onderaan in opcodes.txt te staan:

0B30: 0@ = tutorial_opcode_1_vermenigvuldig 1@ met 2@ 

SCM.ini

De SCM.ini bestanden zijn de configuratiebestanden waarin de opcodes gedefinieerd staan zoals Sannybuilder ze toepast. Het formaat om een opcode te declareren is:

[opcode indentifier]=[aantal parameters],[tekst en parameters]

De parameters kan je gewoon tussen de tekst door zetten op welke plek wat je maar wilt.Waar je wel op moet letten is de index van de parameter, deze bepaald de volgorde van hoe de parameters afgehandeld worden. De index geef je aan met %[index] (natuurlijk zonder de [ ]).Daarnaast moet je ook het type aangeven, deze komt direct achter de index.

De volgende types zijn er:

  • d% = kan elk type zijn
  • p% = pointer
  • o% = object model (alle types)
  • g% = gxt referenties, 8 tekens lang
  • x% = extern script

Open het bestand SASCM.ini, en scrol volledig naar onder.We kunnen bijna hetzelfde erin zetten, als in opcodes.txtAls eerste zetten we dus de indentifier neer: 0B30 (zie bovenstaande code)Zoals ik al aangaf, wil ik dat mijn opcode er op de volgende manier uit komt te zien:

0@ = tutorial_opcode_1_vermenigvuldig 1@ met 2@ 

De parameters zijn allemaal integers, deze worden dus vervangen met een type d%.Omdat we willen dat 1@ het eerste uitgelezen wordt, 2@ als tweede en 0@ als derde, moeten we de index hiervan correct aangeven.

In het formaat wat ik aangaf, zou mijn opcode in het SCM.ini bestand er zo uit zien:

0B30=3,%3d% = tutorial_opcode_1_vermenigvuldig %1d% met %2d%

Voer bovenstaande uit en sla opcodes.txt en de SCM.ini op. Indien je Sannybuilder had opgestart, moet je deze opnieuw opstarten.

Gebruiken in een CLEO script

Om de opcode te gebruiken in een CLEO script, hoeven we simpelweg enkel een script te maken en de opcode hierin te gebruiken zoals wel met elke andere opcode zouden doen:

{$CLEO .cs}
thread 'OPCODE_TUT'
:INIT
1@ = 8
2@ = 5
3@ = 0
:OPCODE_TUT
   wait 50
   if
       0AB0:  key_pressed 0x4F  // Toets O
   then
       if
           0AB0: key_pressed 0x11 // Toets Ctrl
       then
           gosub @EXECUTE_NEW_OPCODE
       end
   end
jump @OPCODE_TUT

:EXECUTE_NEW_OPCODE
 wait 0
 0B30: 3@ = tutorial_opcode_1_vermenigvuldig 1@ met 2@ 
 0ACE: show_formatted_text_box "Opcode resultaat: %d" 3@       
  wait 1000
return
0A93: end_custom_thread

Zoals je ziet: wanneer er op de knoppen: CTRL + O gedrukt wordt, springt het script naar label :EXECUTE_NEW_OPCODE, hier staat de gloednieuwe opcode die uitgevoerd wordt.

Testen

En als laatste stap, starten we het spel op en gaan we het testen.

test_success.png

Tot slot

Bovenstaande informatie kan mogelijk enkele fouten bevatten, wanneer je een fout ziet, meld het dan a.u.b. zodat ik het kan aanpassen. Als je opmerkingen of vragen hebt, post ze gewoon ;)

Bijlage: source bestanden

Bewerkt: door Crypteq
Geplaatst:

Ziet er, zoals al jouw tutorials, weer erg overzichtelijk uit. Ik heb regelmatig nog eens gedacht om te beginnen modden in de oudere GTA's (en dan vooral Vice City), zo'n interessante tutorials wakkeren die interesse nog wat aan, dus wie weet probeer ik het eens uit deze zomer. ^_^ Al is dit natuurlijk geen beginnersmateriaal. :puh:

Geplaatst:

Nice tut! :tu: Moet eerlijk zeggen dat ikzelf nog nooit een opcode heb gemaakt voor scm, ook nog nooit nodig gehad overigens, maar zal binnenkort je tut eens doornemen. :bier:

Geplaatst:

Mooie tutorial! Ik wist niet eens dat dit mogelijk was. :)

Een puntje dat ik opmerkte is dat je soms 'with' gebruikt en soms 'met' in je opcode. Maakt het verschil veel uit?

0@ = tutorial_opcode_1_vermenigvuldig 1@ with 2@

0@ = tutorial_opcode_1_vermenigvuldig 1@ met 2@

Tevens is het C++ deel wat onduidelijk, het is me bijvoorbeeld niet helemaal duidelijk wat elke functie precies doet in het script. (Bv. CLEO_GetOperandType, CLEO_SkipOpcodeParams etc..) ;)

Geplaatst: (bewerkt)

@Hanneswasco

Bedankt :)

Nee, dit is absoluut niet voor beginners, maar daar is deze tutorial ook niet op gericht ;)

@Dutchy3010

Bedankt

In de meeste gevallen heb ik het ook niet nodig gehad, maar wanneer je zelf gaat experimenteren met eigen opcodes, kan je dingen creeren die normaal niet kunnen.

@Basssss

Ook bedankt :puh:

0@ = tutorial_opcode_1_vermenigvuldig 1@ with 2@

Het gebruik van with hierin is een foutje van mij. Ik had als voorbeeld eerst die opcode in het Engels beschreven, en later toch maar in Nederlands. Daarbij heb ik denk ik vergeten om sommige stukken waar with staat te vervangen. Echter maak dat verder voor de werking niet uit, de tekst in opcodes is enkel maar voor de leesbaarheid.

Als je dit er neer zou zetten:

0B30: 3@ 1@ 2@ 

Zou het ook werken, de Sannybuilder compiler kijkt enkel naar de parameters, de tekst doet de compiler helemaal niks mee.

Het deel van C++ is niet echt C++, want de delen van CLEO_[...] zijn de funties die in de CLEO SDK zitten.

Ik heb onder het kopje: Overige CLEO functies globaal beschreven wat die specifieke functie doet.

CLEO_SkipOpcodeParams(CScriptThread* thread, int count)

Bovenstaande opcode bijvoorbeld die slaat het geven aantal parameters over, dat heb ik in de tutorial ook beschreven.

Dit betekent dus: Als je bijvoorbeeld 5 parameters in je opcode hebt (de onderstaande opcode bestaat niet, is enkel ter illustratie):

0B64:  get_location_from_player 0@ 1@ 2@ or_die_at_location 3@ 4@ 5@

En je zou in je code het volgende aanroepen:

CLEO_SkipOpcodeParams(thread, 3);

Dan slaat het in de opcode 0@ (= 1) 1@ (=2) 2@(=3) over, as je hierna een paramater zou uitlezen (of schrijven) dan begint hij in dit geval bij 3@.

Maar als de beschrijving zoals het er staat niet duidelijk is, dan zal ik dat aanpassen :)

Bewerkt: door Crypteq
  • 2 years later...
Geplaatst: (bewerkt)

Zeer mooi gestructureerde tut, kwam goed van pas toen ik een tijdje geleden een plugin moest maken voor iets dat niet in plain CLEO mogelijk was.

Nog een tip: je moet de SCM.ini niet aanpassen (bijvoorbeeld als je het toch maar in 1 script moet gebruiken) als je gebruik maakt van een macro in sannybuilder:

{$O 0B30=3,%3d% = tutorial_opcode_1_vermenigvuldig %1d% met %2d%}

Zet dit bovenaan in je script (onder {$CLEO} of iets dergelijks)

Bewerkt: door robin_be

Een reactie plaatsen

Je kan nu een reactie plaatsen en pas achteraf registreren. Als je al lid bent, log eerst in om met je eigen account een reactie te plaatsen.

Gast
Op dit onderwerp reageren...

×   Je hebt text geplaatst met opmaak.   Opmaak verwijderen

  Only 75 emoji are allowed.

×   Je link is automatisch ingevoegd.   In plaats daarvan weergeven als link

×   Je vorige bewerkingen zijn hersteld.   Alles verwijderen

×   You cannot paste images directly. Upload or insert images from URL.

  • Recent actief   0 leden

    • Er zijn hier geen geregistreerde gebruikers aanwezig.
×
×
  • Create New...