PlayerDictionary vs CachedPlayer

Please post any questions about developing your plugin here. Please use the search function before posting!
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Tue Dec 24, 2019 5:10 pm

InvisibleSoldiers wrote:You're wrong! :mad: This code is nicer to read than original version. also the number of lines of code was decreased. From 179 to 107.

Readability is subjective, but I personally find it much easier to read a code that is not packed solely to decrease the number of lines. For the same reason paragraphs are important in books. Here are some resource you may find constructive:


InvisibleSoldiers wrote:And the listener isn't necessary if you're sure that nothing can remove the 'prop_dynamic_override' entity, only round_restart can. And EntityDictionary itself registers a simlar callback. Retrieving the same dynamic attributes isn't a argument, it's easy can be decided, I did in haste.


I'm talking about the OnPlayerRunCommand listener. Which is extremely noisy as it is called every frame for every players. If there is 30 players on your server, this is like having 30 tick listeners meaning that yes, every calls matters and what your code does behind the scenes is more than important. Take the following example (should be "active_weapon" but that is not the point I'm trying to make):

Syntax: Select all

if self.weapon is not None:
if self.parachute.parent != self.weapon:
self.parachute.parent = self.weapon


You are effectively calling Player.get_active_weapon 3 times which internally request the m_hActiveWeapon property every times. For what? To save a line? This is redundant. Good practice is to use a local assignments and reuse them. Just like I did in the original code:

Syntax: Select all

weapon = player.active_weapon
if weapon is not None:
parent = parachute.parent
if parent != weapon:
parachute.parent = weapon


The weapon is only resolved once, assigned to your local frame and reused; optimization! You are doing the same with fall_velocity, and couldn't reuse it regardless unless you pass it to your class, etc. Same with the self.parachute. Do it once, then work with it. No need to resolve it dozen of times from your self instance.

InvisibleSoldiers wrote:Subclass parachute entity to player object is very well indeed, because a players owns it and it's private, also functions have been defined in Player class which make it clear because they work only with him objects, and it makes the code more in OOP style as Python was planned.
Do you know that any variable which was defined in __init__ method in a class by your definition is the subclassing too? Because int, str, float are classes.

That is not the point. The point is that you already have a player, use it! There is absolutely no reason to add extra layers. This just make the code more complicated, for no benefits, at the cost of performance. Your code add extra casts, extra instantiations, extra methods resolutions, etc. for no benefits, at again, the cost of performance. Keeping everything locally is much faster, for the obvious reason that you do no have to exit the frame and switch from your callback to your class scope. For the same reason from imports are faster than importing the module. For example, when you do:

Syntax: Select all

import math
...
math.radians(...)


Python has to resolve each functions from the module and has to retrieve it dynamically. While when you do the following:

Syntax: Select all

from math import radians
...
radians(...)


Python don't have any resolution to make as it was already resolved in your global scope. I find it rather ironic that you started this thread with timing data as an argument, to then claim performance is not an argument in favour of saving lines. In conclusion, every calls counts. Every resolution counts. Every instantiation counts. If you use subclasses just to claim your code is "pythonic", at the cost of performance, then you are doing it wrong in my opinion.

Anyways, this thread went way out of scope of what it was originally all about so I'm just gonna leave it here as we will have to agree to disagree.

Happy holidays!
User avatar
Ayuto
Project Leader
Posts: 2193
Joined: Sat Jul 07, 2012 8:17 am
Location: Germany

Re: PlayerDictionary vs CachedPlayer

Postby Ayuto » Wed Dec 25, 2019 12:30 pm

InvisibleSoldiers wrote:You're wrong! :mad: This code is nicer to read than original version. also the number of lines of code was decreased. From 179 to 107.

Your version does not seem to reduce the number of lines by using classes, but by removing comments and blank lines that were added for readability. I thought your goal was to increase readability? :confused:

You also converted lines like this:

Syntax: Select all

@OnPlayerRunCommand
def _on_player_run_command(player, usercmd):
if player.is_bot():
return

# ...
to lines like this:

Syntax: Select all

@OnPlayerRunCommand
def _on_player_run_command(player, usercmd):
if player.is_bot(): return
# ...
While readability is mostly subjective, I personally prefer the prior version.

Or

Syntax: Select all

_path = f'models/parachute/{SOURCE_ENGINE}/parachute_default.mdl'
if not (GAME_PATH / _path).isfile():
parachute_model = None
else:
parachute_model = Model(_path)
to

Syntax: Select all

if not (GAME_PATH / f'models/parachute/{SOURCE_ENGINE}/parachute_default.mdl').isfile(): PARACHUTE_MODEL = None # I guess we need unload the plugin if moden isn't found.
else: PARACHUTE_MODEL = Model(f'models/parachute/{SOURCE_ENGINE}/parachute_default.mdl')
Moreover, you add reduandancy in the second example. If you wish to change the default model, you have to update two lines.

By applying only these changes to L'In20Cible's code, I get a similar number of lines.

Btw. you can shorten the last example a little bit more by using an inline if-else expression (not that I would do that):

Syntax: Select all

PARACHUTE_MODEL = Model(f'models/parachute/{SOURCE_ENGINE}/parachute_default.mdl') if (GAME_PATH / f'models/parachute/{SOURCE_ENGINE}/parachute_default.mdl').isfile() else None
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Wed Dec 25, 2019 3:27 pm

Ayuto wrote:Your version does not seem to reduce the number of lines by using classes, but by removing comments and blank lines that were added for readability. I thought your goal was to increase readability? :confused:

Well, yes, it's debatable but that's not the point, point is that overall readability in my opinion improved, also we're talking about plugin from 2014, I don't know how bad was this year, because I just got here recently.

Syntax: Select all

def get_player_parachute(player)
def close_parachute(player)
def parachute_check(player)

now in deserved Player class

Syntax: Select all

class ParachutePlayer
def create_parachute(self)
def process_parachute(self)
def close_parachuse(self)

And what is wrong with the embedded parachute inside the player class?
Do you think it is better?

Syntax: Select all

def get_player_parachute(player):
for parachute in parachutes.values():
if parachute.owner_handle != player.inthandle:
continue
return parachute

We can know clearly whose parachute it is if we put it in the player's object.
That is not the point. The point is that you already have a player, use it! There is absolutely no reason to add extra layers. This just make the code more complicated, for no benefits, at the cost of performance. Your code add extra casts, extra instantiations, extra methods resolutions, etc. for no benefits, at again, the cost of performance. Keeping everything locally is much faster, for the obvious reason that you do no have to exit the frame and switch from your callback to your class scope.

Why do you think so? Why complicated? Perfomance? On the contrary I think it's great to have the parachute like a class attribute. It feels like I can't write something big in Source.Python because I see that every the line is a disaster for you. Should I believe you?

Even that's not the point

The topic of the forum is about a new innovation with internal cache in Entity class, which is still unclear to me. L'In20Cible proved that EntityDictionary is more universally than the internal cache. Of course. I can use EntityDictionary for private entities, as well every entity. I don't need the internal cache because if i want the caching, existing EntityiDictionary which is versatile and suitable for everything will completely satisfy me with its methodology. Yes! There's just some confusion right now.

Syntax: Select all

PLAYERS = PlayerDictionary(TimerPlayer, caching=False)

Thank you! It's like I'm creating a dictionary that as if doesn't cache, fantastic.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Wed Dec 25, 2019 10:39 pm

Boy, this thread is getting silly.

InvisibleSoldiers wrote:Why do you think so? Why complicated? Perfomance? On the contrary I think it's great to have the parachute like a class attribute. It feels like I can't write something big in Source.Python because I see that every the line is a disaster for you. Should I believe you?


I never disagreed with the fact that binding the parachute to the player would be better, I said it would add incertitude about their validity and disagreed with adding extra layers, repeated resolutions, extra frame instantiating, etc. As I said above:

L'In20Cible wrote:The point is that you already have a player, use it! There is absolutely no reason to add extra layers.

For example, there is no reason not to simply do:

Syntax: Select all

# ../addons/source-python/plugins/parachute/parachute.py

# ============================================================================
# >> IMPORTS
# ============================================================================
# Python Imports
# Math
from math import cos
from math import radians
from math import sin

# Source.Python Imports
# Core
from core import SOURCE_ENGINE
# Engines
from engines.precache import Model
# Entities
from entities.constants import MoveType
from entities.entity import Entity
# Listeners
from listeners import OnEntityDeleted
from listeners import OnPlayerRunCommand
# Mathlib
from mathlib import QAngle
from mathlib import Vector
# Paths
from paths import GAME_PATH
# Players
from players.constants import PlayerButtons
from players.constants import PlayerStates
from players.entity import Player
# Stringtables
from stringtables.downloads import Downloadables


# ============================================================================
# >> CONFIGURATION
# ============================================================================
# Define the falling speed in units/s. The higher this value is, the
# faster parachutists will fall.
falling_speed = 32

# Define the speed of the steering lines in units/s. To pull a steering line,
# parachutists must use their A or D keys to gain speed into the right or left
# direction allowing them to have a better control of their landing. However,
# pulling a line come with a falling speed cost.
#
# Set to 0 to disable the ability to pull a line.
steering_lines_speed = 5

# Define the falling speed penalty multiplier when pulling a line.
# e.g. With a falling speed of 32 and a penalty of 1.5, parachutists pulling
# their lines would fall at a speed of 48 units/s.
#
# Set to 1 to disable the falling speed penalty.
steering_lines_penalty = 1.5

# Define the key players must hold to deploy their parachute.
deployment_button = PlayerButtons.SPEED


# ============================================================================
# >> GLOBALS
# ============================================================================
downloadables = Downloadables()
downloadables.add_directory(f'models/parachute/{SOURCE_ENGINE}')
downloadables.add_directory('materials/models/parachute')

_path = f'models/parachute/{SOURCE_ENGINE}/parachute_default.mdl'
if not (GAME_PATH / _path).isfile():
parachute_model = None
else:
parachute_model = Model(_path)


# ============================================================================
# >> LISTENERS
# ============================================================================
@OnPlayerRunCommand
def _on_player_run_command(player, usercmd):
"""Called when a player runs a command."""
if player.is_bot():
return

fall_velocity = player.fall_velocity
if (player.dead or
player.move_type == MoveType.LADDER or
player.flags & PlayerStates.INWATER or
not usercmd.buttons & deployment_button or
fall_velocity < 1.0):

parachute = getattr(player, 'parachute', None)
if parachute is None:
return
parachute.remove()
player.parachute = None
parachute.parachutist = None
return

side_move = usercmd.side_move
if side_move and steering_lines_speed > 1:
yaw = player.eye_angle.y

if side_move < 0:
yaw += 90
else:
yaw -= 90
yaw = radians(yaw)

vec = Vector(
x=cos(yaw) * steering_lines_speed,
y=sin(yaw) * steering_lines_speed,
z=fall_velocity + -(falling_speed * steering_lines_penalty))
else:
vec = Vector(z=fall_velocity + -falling_speed)

player.base_velocity = vec

if parachute_model is None:
return

angles = QAngle(y=player.eye_angle.y)
parachute = getattr(player, 'parachute', None)
if parachute is None:
parachute = Entity.create('prop_dynamic_override')
player.parachute = parachute

weapon = player.active_weapon
if weapon is not None:
parachute.parent = weapon

parachute.model = parachute_model
parachute.parachutist = player
parachute.origin = player.origin
parachute.angles = angles
parachute.model_scale = 0.7

parachute.spawn()
return

weapon = player.active_weapon
if weapon is not None:
parent = parachute.parent
if parent != weapon:
parachute.parent = weapon

origin = player.origin
else:
parachute.parent = None
view_offset = player.view_offset.copy()
view_offset.z /= 2
origin = player.origin + view_offset

parachute.origin = origin
parachute.angles = angles

if parachute.model_scale >= 1.0:
return

parachute.model_scale += 0.024


@OnEntityDeleted
def _on_entity_deleted(base_entity):
"""Called when an entity is being deleted."""
try:
index = base_entity.index
except ValueError:
return

parachute = Entity(index)
parachutist = getattr(parachute, 'parachutist', None)

if parachutist is None:
return

parachutist.parachute = None
parachute.parachutist = None


The parachute is bound to the player, and the player is bound to the parachute. No extra layers, you remain local and reuse your assignments, etc. Doesn't change the fact that I referred that plugin as an example usage of the EntityDictionary illustrating the fact that is it a convenience container first and foremost unrelated to the internal cache. I referred to an example because after repeating it like 4 times it was still not understood so I though a code example would explain it better than myself. You really just took that example, derailed the entire thread, in an effort to discredit the usage so that it suit your argument.

InvisibleSoldiers wrote:Even that's not the point

The topic of the forum is about a new innovation with internal cache in Entity class, which is still unclear to me. L'In20Cible proved that EntityDictionary is more universally than the internal cache. Of course. I can use EntityDictionary for private entities, as well every entity. I don't need the internal cache because if i want the caching, existing EntityiDictionary which is versatile and suitable for everything will completely satisfy me with its methodology. Yes! There's just some confusion right now.


As stated above:

L'In20Cible wrote:you don't seem to understand that the goal was to optimize existing codes


The internal cache improves existing codes. That's it. Nothing more, nothing less than that. Not sure how many times it will need to be repeated.

InvisibleSoldiers wrote:

Syntax: Select all

PLAYERS = PlayerDictionary(TimerPlayer, caching=False)

Thank you! It's like I'm creating a dictionary that as if doesn't cache, fantastic.


The EntityDictionary simply passes the given arguments and keywords to your factory. If that is bothering you to write it like that, you can pass it from your class directly. For example:

Syntax: Select all

class TimerPlayer(Player):
def __init__(index):
super().__init__(index, caching=False)

PLAYERS = PlayerDictionary(MyPlayer)


Not that there is really anything wrong using the internal cache along with your dictionary. This really just mean an incref of 1, and allows to get the same objects from your class constructor directly.
Sam
Senior Member
Posts: 100
Joined: Tue Jul 03, 2018 3:00 pm
Location: *DELETED*
Contact:

Re: PlayerDictionary vs CachedPlayer

Postby Sam » Thu Dec 26, 2019 5:05 am

What's going on here xDDD
Last edited by Sam on Thu Dec 26, 2019 5:05 am, edited 1 time in total.
Reason: Original post version
Sam
Senior Member
Posts: 100
Joined: Tue Jul 03, 2018 3:00 pm
Location: *DELETED*
Contact:

Re: PlayerDictionary vs CachedPlayer

Postby Sam » Thu Dec 26, 2019 5:07 am

It all started with a comparison, but ended almost in a quarrel xD
Last edited by Sam on Thu Dec 26, 2019 5:07 am, edited 1 time in total.
Reason: Original post version
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Fri Dec 27, 2019 7:21 am

Sam wrote:It all started with a comparison, but ended almost in a quarrel xD

Yeah, mainly two headstrongs going at it because they are not looking from the same angles. :smile:

I mean, although I truly believe he is making a huge deal out of nothing, I can (sorta) understand where he is coming from, that the internal caching is similar to a defaulted EntityDictionary, but they still have clearly distinct use cases from my perspective. Either way, just to picture it, all the followings (thanks to GitHub new reference features) are now performing faster due to the internal caching, along with all existing plugins that use entities, players, and weapons objects, which is more than enough to justify any features/changes being added/implemented regardless if they already have similarities with an existing API. Not to mention that the #1 priority is: backward compatibility.

InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Sun Dec 29, 2019 11:02 am

Entity.cache isn't even connected with EntityDictionary and this is completely new which located in Entity.class but repeats the EntityDictionary behavior.

I still doubt this decision because you answer unconvincingly.
I can't avoid using Player.cache even with caching=False because every time when I wish to retrieve a entity object through any way (EntityDictionary, PlayerDictionary) code in Entity class will check variable caching although a priori knowledge of using EntityDictionary
I think this is really an unnecessary layer.
But I understand your main reason for it improve code that explicitly creates a new object:

Syntax: Select all

Player(1)
Entity(129)


You can then create a separate object for this with type of EntityDictionary and the object will avariable for import in any area.
I don't know, it's all weird at all or I'm really silly boy.

Moved from https://github.com/Source-Python-Dev-Te ... n/pull/295
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Sun Dec 29, 2019 11:16 am

InvisibleSoldiers wrote:I can't avoid using Player.cache even with caching=False because every time when I wish to retrieve a entity object through any way (EntityDictionary, PlayerDictionary) code in Entity class will check variable caching although a priori knowledge of using EntityDictionary

Do you have a code example?

EDIT: Figured it, should be fixed into 02f9bf60b.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Sun Dec 29, 2019 12:50 pm

I've come up with something. What if we will have the global cache like L'In20Cible suggested, but very very global. I mean caching at low-level CBaseEntity, CBasePlayer, PlayerMixin in C++ and maybe Python with standard attributes and functions from Source-SDK. And nothing more won't be available to create the objects again and again besides the low-level cache.

We pass EntityDictionary, PlayerDictionary like seperate classes. And toggling caching for base c++ classes globally.
Also all listeners should pass base classes only, we can wrap them or use by default like a game.
So we really are closer to Source-SDK and entities are only from c++ , but we have optional feature to wrap them through our classes cached or not cached.

Here's what I suggest at all in one picture

Syntax: Select all

from listeners import OnPlayerRunCommand
from ??? import BasePlayer
from ??? import BaseEntity
from ??? import Caching

class CachedPlayerWrap(BasePlayer, Caching):
def __init__(self, base_player):

super().__init__(base_player.index)

self.variable = 5

class PlayerWrap(BasePlayer):
def __init__(self, baseplayer):

super().__init__(baseplayer.index)

self.variable = 5

def set_view_entity(self, entity):
self.view_coordinates = entity.origin


@OnPlayerRunCmd
def on_player_run_cmd(baseplayer, user_cmd):
base_player.origin
base_player.velocity
# Raises error
base_player.custom_variable

# Already cached somewhere or will be created and cached
player = CachedPlayerWrap(baseplayer)

# From CBaseEntity
player.origin
# From CBaseEntity
player.velocity
# From Wrap
player.custom_variable

# Not cached class, will be created.
player = PlayerWrap(baseplayer)
player.origin
player.velocity
player.custom_variable

# Already cached
player.set_view_entity(BaseEntity(129))
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Mon Dec 30, 2019 7:04 am

What is this supposed to solve or improve exactly?
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 30, 2019 8:11 am

In first, what if there will be cache in C++ side classes like
CBasePlayer, CBaseEntity and on Python side we will simple retrieve them through BasePlayer(1), BaseEntity(1) respectively. So it will be really standard classes cached by default in C++ and nothing more for fast access.
But what about custom properties? If we want it, we can create class which will be inherit from the BasePlayer or BaseEntity, but for now we should only cache variables to read them from a tick to tick. So it unessary at all to define our self class, and we can create simple dict which retrieve index. Or if you want it anyway you inherit from BasePlayer when define the class and then just NewPlayer(index) or NewPlayer(baseplayer) if you need new functions. And if you want using custom variables , save them and load you can use only a dict or use CachedNewPlayer(index) or legacy PlayerDictionary(1). That is. So we improved access to standard classes from SDK (CBasePlayer, CBaseEntity) and did clear defining self classes. And current Player and Entity classes shouldn't be really cached.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 30, 2019 8:24 am

By the way I noticed that there exists pybind11, and maybe we can replace Boost.Python to pybind 11. Quote from stackoverfow
I would recommend PyBind11. I am using it for similar use-case where Python modules calls C++ to perform operations which are costlier and performance extensive. Boost Python is a richer library with size cost where as PyBind11 is header only and it supports STL which makes life easier to pass on basic data structure without writing any code!

Edit: Found better variant PyPy(in 4x-6x) faster than Cython with CFFI for c++ bindings.
Last edited by InvisibleSoldiers on Mon Dec 30, 2019 8:36 am, edited 1 time in total.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Mon Dec 30, 2019 8:36 am

InvisibleSoldiers wrote:In first, what if there will be cache in C++ side classes like
CBasePlayer, CBaseEntity and on Python side we will simple retrieve them through BasePlayer(1), BaseEntity(1) respectively. So it will be really standard classes cached by default in C++ and nothing more for fast access.

The reality is that this would not make them faster, this would make them slower. Currently, they are simply wrapping pointers. If we wanted to cache them, we would have to map and assign the objects to their respective pointer and constantly look them up, compare, etc. which would add a lot of extra looping and computing behind the scenes. Also, BaseEntity can either be networked or server-side only entities, while Entity are only networked.

InvisibleSoldiers wrote:By the way I noticed that there exists pybind11, and maybe we can replace Boost.Python to pybind 11. Quote from stackoverfow
I would recommend PyBind11. I am using it for similar use-case where Python modules calls C++ to perform operations which are costlier and performance extensive. Boost Python is a richer library with size cost where as PyBind11 is header only and it supports STL which makes life easier to pass on basic data structure without writing any code!

Nah. I've worked with Pybind11 in the past and it is far from being as near as what Boost has to offer. Without mentioning that Boost is a full suite offering much much much more. Also, Source.Python on Windows has to be built on VC++2010, and Pybind11 only supports VS2015+. Oh, and I doubt anyone would be up for the task of converting and rewriting Source.Python to a totally different bindings API.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 30, 2019 8:40 am

Edit: Found better variant PyPy(in 4x-6x) faster than Cython with CFFI for c++ bindings.
I just know.a one person who doesn't want to try Source.Python because it is really slow by him opinion.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: PlayerDictionary vs CachedPlayer

Postby L'In20Cible » Mon Dec 30, 2019 8:52 am

InvisibleSoldiers wrote:Edit: Found better variant PyPy(in 4x-6x) faster than Cython with CFFI for c++ bindings.
I just know.a one person who doesn't want to try Source.Python because it is really slow by him opinion.

I've never worked with PyPy before, but I doubt the differences would be phenomenal. However, if you are up for the challenge, please be my guest and make a pull request so that we can compare. Keep in mind, all existing codes must still work, and no new issues must be introduced. I honestly don't think you realize the extent of work it would requires. :tongue:
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 30, 2019 8:57 am

Of course I can't do it myself, but I still want to try making a Lua bindings.

Return to “Plugin Development Support”

Who is online

Users browsing this forum: Bing [Bot] and 19 guests