Extension interoperability

Post here your questions about SFS2X. Here we discuss all server-side matters. For client API questions see the dedicated forums.

Moderators: Lapo, Bax

Sanity1993
Posts: 21
Joined: 30 Jan 2018, 21:14

Extension interoperability

Postby Sanity1993 » 07 Mar 2018, 08:59

Hello,
I am trying to use Extension Interoperability, so i can split down the character information pull from the database into multiple extensions so they can be reused for loading an enemy character without pulling there inventory. I know i could create another extension and copy the code but i dont think thats right to do. It's going to become a very long script accounting for each Item type amongst other things. Especially if all we need to know is the enemy CharacterProperties.

So i have a main class in the RPG Example. GetModelhandler, this pulls 5 hashtables of information for all the information for the character. I have created another script to pull only CharacterProperties but i cannot seem to get the GetModelHandler to call the GetCharProperties.

So checking the example on http://docs2x.smartfoxserver.com/Extens ... w#advanced

I have the below in a second extension

Code: Select all

@Override
    public Object handleInternalMessage(User sender, ISFSObject params)
{
        etc.
    }
   

How ever as the code isnt within the main extension I want the Zone extension to pass to the extension that will control which data to query to then query any of the 5 internalmessages.

Code: Select all

MyZoneExtension zoneExt = (MyZoneExtension) getParentZone().getExtension();
 
Vec3D vec = zoneExt.handleInternalMessage("test", someVec3D);


From my understanding grabs the parent zone extension zoneExt and then handles the internal event.

What i am unsure is how i can do that when i need to instantiate a new GetCharProperties.

Any help appreciated, Thank you.
User avatar
Lapo
Site Admin
Posts: 20043
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Extension interoperability

Postby Lapo » 07 Mar 2018, 09:52

Hi,
what you're doing in the code example looks correct, but I need the details of what you're trying to do. A code one-liner with no references to errors/exceptions is not enough.

Can you please show the actual call you're running and what error you're getting?

Thanks
Lapo
--
gotoAndPlay()
...addicted to flash games
Sanity1993
Posts: 21
Joined: 30 Jan 2018, 21:14

Re: Extension interoperability

Postby Sanity1993 » 07 Mar 2018, 10:46

Ah okay! The full code of GetCharProperties:

Code: Select all

package altarianonline;

import altarianonline.model.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import com.smartfoxserver.v2.annotations.Instantiation;
import com.smartfoxserver.v2.annotations.Instantiation.InstantiationMode;
import com.smartfoxserver.v2.core.ISFSEvent;
import com.smartfoxserver.v2.db.IDBManager;
import com.smartfoxserver.v2.entities.User;
import com.smartfoxserver.v2.entities.data.ISFSArray;
import com.smartfoxserver.v2.entities.data.ISFSObject;
import com.smartfoxserver.v2.entities.data.SFSArray;
import com.smartfoxserver.v2.entities.data.SFSObject;
import com.smartfoxserver.v2.exceptions.SFSException;
import com.smartfoxserver.v2.extensions.BaseClientRequestHandler;
import com.smartfoxserver.v2.extensions.BaseServerEventHandler;
import com.smartfoxserver.v2.extensions.ExtensionLogLevel;
import java.sql.SQLException;

@Instantiation(InstantiationMode.SINGLE_INSTANCE)
public class GetCharProperties extends BaseClientRequestHandler
{
 
public Map<String, CharacterProperty> handleInternalMessage(User sender, ISFSObject params)
{
        Map<String, CharacterProperty> props1 = new HashMap<>();
       
        IDBManager dbManager = getParentExtension().getParentZone().getDBManager();
        String sql = "SELECT * FROM `character` WHERE owner ='"+ sender;
                try{
                ISFSArray res = dbManager.executeQuery(sql);
               
                for (int i = 0; i < res.size(); i++){
                ISFSObject item = res.getSFSObject(i);
                //assign values
                int id = item.getInt("id");//Do nothing with
                int dead = item.getInt("dead");//Do nothing with
               
                String cOwner = item.getUtfString("Owner");//Do nothing with
                int cType = item.getInt("chartype");
               
                int maxHP = item.getInt("maxHP");
                int cHP = item.getInt("cHP");
               
                int mDef = item.getInt("MDef");
                int cmDef = item.getInt("cMDef");
               
                int pDef = item.getInt("PDef");
                int cpDef = item.getInt("cPDef");
               
                int cExp = item.getInt("CurExp");
                int Exp = item.getInt("Exp");
               
                int level = item.getInt("level");   
                int clevel = item.getInt("clevel");   
               
                int stam = item.getInt("stam");
                int cstam = item.getInt("cstam");
               
                int mag = item.getInt("mag");
                int cmag = item.getInt("cmag");
               
                int bp = item.getInt("bp"); 
                int cbp = item.getInt("cbp");
               
      props1.put("Health", new CharacterProperty(cHP, maxHP, "hp"));
      props1.put("Physical Attack", new CharacterProperty(cstam, stam, "patk"));
      props1.put("Magical Attack", new CharacterProperty(cmag, mag, "matk"));
      props1.put("Level", new CharacterProperty(clevel, level, "lvl"));
                props1.put("Magical Defence", new CharacterProperty(cmDef, mDef, "mdef"));
      props1.put("Physical Defence", new CharacterProperty(cpDef, pDef, "pdef"));
      props1.put("Dodge", new CharacterProperty(70, 100, "dge"));
               
                }
                    }
                    catch (SQLException e)
                    {
                          trace(ExtensionLogLevel.WARN,
                            "SQL Failed: " + e.toString());   
                    }
            return props1;
        }

    @Override
    public void handleClientRequest(User user, ISFSObject isfso) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

   
}


So i think as its not a room extension but a zone extension calling it.

Code: Select all

 AltarianOnline zoneExt = (AltarianOnline) getParentZone().getExtension();
                Map<String, Item> inventory1 = zoneExt.handleInternalMessage("GetCharProperties", params);


So i get an error on getParentZone() of cannot find symbol - which i dont think is right, removing parentZone i think will break the getextension.

The line calling the internalMessage i get an error of: incompatible types: Object cannot be converted to Map<String,Item>
Im not using an object so slightly confused.
User avatar
Lapo
Site Admin
Posts: 20043
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Extension interoperability

Postby Lapo » 07 Mar 2018, 11:12

Can you show me the actual stack trace of the error?

The problem with interoperability 99% of the times boils down to the fact that Extensions live in separate class loaders. If you want multiple Extensions to pass around custom objects (i.e. objects not loaded by the top classloader) you will need to deploy your shared classes in the extension/__lib__/ folder so that they're seen at top level.

Alternatively you can deploy the whole Extension jar in that folder (__lib__)

We discuss this in details in the docs:
http://docs2x.smartfoxserver.com/Extens ... assLoading

Cheers
Lapo

--

gotoAndPlay()

...addicted to flash games
Sanity1993
Posts: 21
Joined: 30 Jan 2018, 21:14

Re: Extension interoperability

Postby Sanity1993 » 07 Mar 2018, 21:18

Okay so the actual error is:
ant -f D:\\Documents\\NetBeansProjects\\AltarianOnline -Dnb.internal.action.name=build jar
init:
Deleting: D:\Documents\NetBeansProjects\AltarianOnline\build\built-jar.properties
deps-jar:
Updating property file: D:\Documents\NetBeansProjects\AltarianOnline\build\built-jar.properties
Compiling 1 source file to D:\Documents\NetBeansProjects\AltarianOnline\build\classes
D:\Documents\NetBeansProjects\AltarianOnline\src\altarianonline\GetModelHandler.java:47: error: cannot find symbol
AltarianOnline zoneExt = (AltarianOnline) getParentZone().getExtension();
symbol: method getParentZone()
location: class GetModelHandler
D:\Documents\NetBeansProjects\AltarianOnline\src\altarianonline\GetModelHandler.java:48: error: incompatible types: Object cannot be converted to Map<String,Item>
Map<String, Item> inventory1 = zoneExt.handleInternalMessage("GetCharProperties", params);
D:\Documents\NetBeansProjects\AltarianOnline\src\altarianonline\GetModelHandler.java:57: warning: [deprecation] executeQuery(String) in IDBManager has been deprecated
ISFSArray res = dbManager.executeQuery(sq2);
D:\Documents\NetBeansProjects\AltarianOnline\src\altarianonline\GetModelHandler.java:117: warning: [deprecation] executeQuery(String) in IDBManager has been deprecated
ISFSArray res = dbManager.executeQuery(sql);
2 errors
2 warnings
D:\Documents\NetBeansProjects\AltarianOnline\nbproject\build-impl.xml:930: The following error occurred while executing this line:
D:\Documents\NetBeansProjects\AltarianOnline\nbproject\build-impl.xml:270: Compile failed; see the compiler error output for details.
BUILD FAILED (total time: 0 seconds)



The Java wont compile so unable to produce a stack trace from SFS Logs.

What im trying to do is have an handleClientRequest call multiple handleInternalMessage(From different Java Classes) e.g Inventory,CharacterProperties... depending on the Character data required. The initial handleClientRequest is at zone level not room level.

This is so i can have set class do a single thing so code is easier to maintain. I have read and i hope understood the material you provided.
Your saying the _lib_ folder is part of the zone GlobalClassLoader and the room extension can call it by going through the zone extension.

So
AltarianOnline zoneExt = (AltarianOnline) getParentZone().getExtension();
Map<String, Item> inventory1 = zoneExt.handleInternalMessage("GetCharProperties", params);


"AltarianOnline" is the ZoneExtension i have.
"zoneExt" is the variablename.
"getParentZone().getExtension()" is getting the parent extension and will set zoneExt which is typecasted into AltarianOnline.(I think theres an import im missing there.)

"Map<String, Item>" is the type of dataset i need returned by the internalmessage.
"inventory1" is the variablename.
"zoneExt.handleInternalMessage("GetCharProperties", params);" calls the zoneExtension and method "handleInternalMessage" passing the cmdname and Object needed by the constructor.

Hope that was some help, i hope i understand the concept right. And it provides more of what im trying to do.
User avatar
Lapo
Site Admin
Posts: 20043
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Extension interoperability

Postby Lapo » 08 Mar 2018, 14:08

The Java wont compile so unable to produce a stack trace from SFS Logs.

Yeah, that was the first point of confusion. If it was a runtime or compile time error.

Are you using a Java IDE? I'd highly recommend any of the usual ones such as Eclipse, Netbeans or IntelliJ. They will help heaps with the coding. Like in this case it seems you're struggling because you don't have code hinting showing which methods are available.

I think the error above is due to the fact that your class extends BaseClientRequestHandler, which does not have a getParentZone() method.
It doesn't because that's not your main extension class.
Instead you have a method called getParentExtension() which gives you access to the main Extension class.

All this would be a non issue with a decent java editor ;) :)

Hope it helps
Lapo

--

gotoAndPlay()

...addicted to flash games
Sanity1993
Posts: 21
Joined: 30 Jan 2018, 21:14

Re: Extension interoperability

Postby Sanity1993 » 11 Mar 2018, 00:14

Hey Lapo,
I am using NetBeans 8.2 :P

Playing with MMORoomDermoExtension as it has handle internal event. Running an unaltered version. I believe it may not work anymore?

The two below unaltered java code from the example.

Code: Select all

private class UserVariablesHandler extends BaseServerEventHandler
   {
      @Override
      public void handleServerEvent(ISFSEvent event) throws SFSException
      {
         @SuppressWarnings("unchecked")
                        List<UserVariable> variables = (List<UserVariable>) event.getParameter(SFSEventParam.VARIABLES);
         User user = (User) event.getParameter(SFSEventParam.USER);
         
         // Make a map of the variables list
         Map<String, UserVariable> varMap = new HashMap<String, UserVariable>();
         for (UserVariable var : variables)
         {
            varMap.put(var.getName(), var);
         }
         
         if (varMap.containsKey("x") && varMap.containsKey("z"))
         {
            Vec3D pos = new Vec3D
            (
               varMap.get("x").getDoubleValue().floatValue(),
               1.0f,
               varMap.get("z").getDoubleValue().floatValue()
            );
            
            mmoAPi.setUserPosition(user, pos, getParentRoom());
         }
      }
   }


Code: Select all

private UserVariablesHandler userVariablesHandler;
   private NPCRunner npcRunner;
   private ISFSMMOApi mmoAPi;
   
   private ScheduledFuture<?> npcRunnerTask;
   private List<User> allNpcs;
   
   @Override
   public void init()
   {
       userVariablesHandler = new UserVariablesHandler();
       npcRunner = new NPCRunner();
       mmoAPi = SmartFoxServer.getInstance().getAPIManager().getMMOApi();
      
       addEventHandler(SFSEventType.USER_VARIABLES_UPDATE, userVariablesHandler);


These produce:
00:22:22,492 INFO [main] managers.SFSRoomManager - Room created: { Zone: BasicExamples }, [ MMORoom: The Lobby, Id: 0, Group: default, AOI: (100, 100, 0) ], type = MMORoom
java.lang.NullPointerException
at com.smartfoxserver.v2.api.SFSApi.createNPC(SFSApi.java:688)
at sfs2x.extension.mmo.MMORoomDemoExtension.simulatePlayers(MMORoomDemoExtension.java:158)
at sfs2x.extension.mmo.MMORoomDemoExtension.init(MMORoomDemoExtension.java:131)
at com.smartfoxserver.v2.entities.managers.SFSExtensionManager.createExtension(SFSExtensionManager.java:303)
at com.smartfoxserver.v2.entities.managers.SFSZoneManager.createZone(SFSZoneManager.java:426)
at com.smartfoxserver.v2.entities.managers.SFSZoneManager.initializeZones(SFSZoneManager.java:239)
at com.smartfoxserver.v2.SmartFoxServer.start(SmartFoxServer.java:292)
at com.smartfoxserver.v2.Main.main(Main.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at com.exe4j.runtime.LauncherEngine.launch(Unknown Source)
at com.exe4j.runtime.WinLauncher.main(Unknown Source)
at com.install4j.runtime.launcher.WinLauncher.main(Unknown Source)
User avatar
Lapo
Site Admin
Posts: 20043
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Extension interoperability

Postby Lapo » 12 Mar 2018, 08:12

I think you're running the example code the wrong way.
In our demo we attach the MMO Extension to a Room at runtime, after the user has connected and logged in.

Here it looks like you're starting up the Extension when the server is booting up, which will cause the error above. NPCs can't be created before the server boot is complete.

To avoid errors like that you will need to add a listener for the SFSEventType.SERVER_READY in your Extension's init() and then execute the Room creation code there.

Hope it helps.
Lapo

--

gotoAndPlay()

...addicted to flash games
Sanity1993
Posts: 21
Joined: 30 Jan 2018, 21:14

Re: Extension interoperability

Postby Sanity1993 » 12 Mar 2018, 19:56

Hmmm Lapo, my rooms i need this is are actually static and predefined from the server not created from the client if no room is available. From what i can see the addEventListner is actually client side?? to ensure the server is ready to accept connections.

This my my actual code in the LobbyRoomExtension i got to test with.

Code: Select all

package altarianonline;

import com.smartfoxserver.v2.SmartFoxServer;
import com.smartfoxserver.v2.api.ISFSMMOApi;
import com.smartfoxserver.v2.core.ISFSEvent;
import com.smartfoxserver.v2.core.SFSEventParam;
import com.smartfoxserver.v2.core.SFSEventType;
import com.smartfoxserver.v2.entities.User;
import com.smartfoxserver.v2.entities.variables.UserVariable;
import com.smartfoxserver.v2.exceptions.SFSException;
import com.smartfoxserver.v2.extensions.BaseServerEventHandler;
import com.smartfoxserver.v2.extensions.SFSExtension;
import com.smartfoxserver.v2.mmo.MMORoom;
import com.smartfoxserver.v2.mmo.Vec3D;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LobbyRoomExtension extends SFSExtension
{
        private SmartFoxServer sfs;
   private ISFSMMOApi mmoApi;
   private MMORoom room;
        private UserVariablesHandler userVariablesHandler;
       
        private class UserVariablesHandler extends BaseServerEventHandler
                {
      @Override
      public void handleServerEvent(ISFSEvent event) throws SFSException
      {
                       trace("UserVariablesHandler called");
                       
     
                        @SuppressWarnings("unchecked")
                        List<UserVariable> variables = (List<UserVariable>) event.getParameter(SFSEventParam.VARIABLES);
                        User user = (User) event.getParameter(SFSEventParam.USER);

                         // Make a map of the variables list
                         Map<String, UserVariable> varMap = new HashMap<String, UserVariable>();
                         for (UserVariable var : variables)
                         {
                            varMap.put(var.getName(), var);
                         }

                         if (varMap.containsKey("x") && varMap.containsKey("z"))
                         {
                            Vec3D pos = new Vec3D
                            (
                               varMap.get("x").getDoubleValue().floatValue(),
                               varMap.get("y").getDoubleValue().floatValue(),
                               varMap.get("z").getDoubleValue().floatValue()
                            );

                            mmoApi.setUserPosition(user, pos, getParentRoom());
                         }
                      }
                    }
   
       
    @Override
    public void init() {
                room = (MMORoom) this.getParentRoom();
      
      // Get a reference to the SmartFoxServer instance
      sfs = SmartFoxServer.getInstance();
      
      // Get a reference to the MMO dedicated API
      mmoApi = sfs.getAPIManager().getMMOApi();
                //addEventListener(SFSEventType.SERVER_READY);
               
                addEventHandler(SFSEventType.USER_VARIABLES_UPDATE, userVariablesHandler);
                 }
               
               
    @Override
    public void destroy()
    {

    }
   
}


Found it server side!
Here is what i tested...

Code: Select all

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package altarianonline;

import com.smartfoxserver.v2.SmartFoxServer;
import com.smartfoxserver.v2.api.ISFSMMOApi;
import com.smartfoxserver.v2.core.ISFSEvent;
import com.smartfoxserver.v2.core.SFSEventParam;
import com.smartfoxserver.v2.core.SFSEventType;
import com.smartfoxserver.v2.entities.User;
import com.smartfoxserver.v2.entities.variables.UserVariable;
import com.smartfoxserver.v2.exceptions.SFSException;
import com.smartfoxserver.v2.extensions.BaseServerEventHandler;
import com.smartfoxserver.v2.extensions.SFSExtension;
import com.smartfoxserver.v2.mmo.MMORoom;
import com.smartfoxserver.v2.mmo.Vec3D;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 * @author Scott Allsup
 */
public class LobbyRoomExtension extends SFSExtension
{
        private SmartFoxServer sfs;
   private ISFSMMOApi mmoApi;
   private MMORoom room;
        private UserVariablesHandler userVariablesHandler;
       
        private class UserVariablesHandler extends SFSExtension
                {
      @Override
      public void handleServerEvent(ISFSEvent event) throws SFSException
      {
                       trace("UserVariablesHandler called");
                        List<UserVariable> variables = (List<UserVariable>) event.getParameter(SFSEventParam.VARIABLES);
                        User user = (User) event.getParameter(SFSEventParam.USER);

                         // Make a map of the variables list
                         Map<String, UserVariable> varMap = new HashMap<String, UserVariable>();
                         for (UserVariable var : variables)
                         {
                            varMap.put(var.getName(), var);
                         }

                         if (varMap.containsKey("x") && varMap.containsKey("y") && varMap.containsKey("z"))
                         {
                            Vec3D pos = new Vec3D
                            (
                               varMap.get("x").getDoubleValue().floatValue(),
                               varMap.get("y").getDoubleValue().floatValue(),
                               varMap.get("z").getDoubleValue().floatValue()
                            );

                            mmoApi.setUserPosition(user, pos, getParentRoom());
                         }
                }

        @Override
        public void init() {
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }
                    }
   
       
    @Override
    public void init() {
                room = (MMORoom) this.getParentRoom();
      
      // Get a reference to the SmartFoxServer instance
      sfs = SmartFoxServer.getInstance();
      
      // Get a reference to the MMO dedicated API
      mmoApi = sfs.getAPIManager().getMMOApi();
                //addEventListener(SFSEventType.SERVER_READY);
               
                addEventListener(SFSEventType.USER_VARIABLES_UPDATE, userVariablesHandler);
                 }
               
               
    @Override
    public void destroy()
    {

    }
   
}


Which unfortunately produced

20:07:43,855 WARN [SFSWorker:Ext:3] managers.SFSExtensionManager - java.lang.NullPointerException:
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Exception: java.lang.NullPointerException
Message: *** Null ***
Description: Error during event handling: java.lang.NullPointerException, Listener: null
+--- --- ---+
Stack Trace:
+--- --- ---+
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.dispatchEvent(SFSExtensionManager.java:768)
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.dispatchRoomLevelEvent(SFSExtensionManager.java:710)
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.handleServerEvent(SFSExtensionManager.java:969)
com.smartfoxserver.v2.core.SFSEventManager$SFSEventRunner.run(SFSEventManager.java:65)
java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
java.lang.Thread.run(Unknown Source)
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
User avatar
Lapo
Site Admin
Posts: 20043
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Extension interoperability

Postby Lapo » 13 Mar 2018, 09:26

Sanity1993 wrote:Hmmm Lapo, my rooms i need this is are actually static and predefined from the server not created from the client if no room is available. From what i can see the addEventListner is actually client side??

No, you can listen to server-side events.
Check the docs:
http://docs2x.smartfoxserver.com/Extens ... uick-start
Server Side events section

Also in your code you're overriding the handleServerEvent method which is the incorrect way of listening for events.
Please refer to the docs.

Cheers
Lapo

--

gotoAndPlay()

...addicted to flash games
Sanity1993
Posts: 21
Joined: 30 Jan 2018, 21:14

Re: Extension interoperability

Postby Sanity1993 » 13 Mar 2018, 19:33

» Server side events

In addition to handling client requests a server-side Extension can also listen for a number of Server's events, such as login events, logout, join room and many more.

Listening for server events is as simple as handling client requests: we just create a function and register it as event handler. Here's a basic example with no purpose other than demonstrating this functionality:

public class MyExtension extends SFSExtension
{
@Override
public void init()
{
trace("Hello, this is my first SFS2X Extension!");

// Add a new Event Handler
addEventHandler(SFSEventType.USER_JOIN_ZONE, ZoneEventHandler.class);
}

@Override
public void destroy()
{
trace("Destroy is called!");
}

public class ZoneEventHandler extends BaseServerEventHandler
{
@Override
public void handleServerEvent(ISFSEvent event) throws SFSException
{
User user = (User) event.getParameter(SFSEventParam.USER);
trace("Welcome new user: " + user.getName());
}
}
}

Each event provides a number of parameters that can be accessed as shown in the code. For a full list of events, check the documentation here.


erm... It says @Override!
User avatar
Lapo
Site Admin
Posts: 20043
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Extension interoperability

Postby Lapo » 14 Mar 2018, 08:19

But in your code you are not extending the BaseServerEventHandler class, instead you've created another SFSExtension class inside the top extension class.

I'd start fixing that.
Lapo

--

gotoAndPlay()

...addicted to flash games

Return to “SFS2X Questions”

Who is online

Users browsing this forum: No registered users and 5 guests