IIYAMA

[ENCHANTMENT] client - Render events enchanting

Recommended Posts

Render events enchanting

  • Having a lot of render events in your resource?
  • Easier attach and remove?
  • Or do you want to pass arguments in to a function which is attached to a render event?

Then this might be something for you.

 


Syntax: addRenderEvent

bool addRenderEvent(function functionVar [, string eventName, arguments ...])

Arguments:

  1. The function you want to attach/target.
  2. The event you want to use. ( "onClientRender", "onClientPreRender", "onClientHUDRender") If you do not fill in one of these three, it will automatic fallback to "onClientRender". Fool proof.
  3. Arguments you can pass to the target function. (which isn't possible with the default addEventHandler + onClientRender function)

Returns: true when added, and false otherwise.


Syntax: removeRenderEvent

bool removeRenderEvent(function functionVar [, string eventName])

Arguments:

  1. The function you want to attach/target.
  2. The event you want to use. ( "onClientRender", "onClientPreRender", "onClientHUDRender") If you do not fill in one of these three, it will automatic fallback to "onClientRender". Fool proof.

Returns: true if found + removed, and false otherwise.

(Not recommended to execute this function every frame > performance)


onClientPreRender + timeslice

If you use "onClientPreRender", just like the default event, it will pass the timeSlice to the attached/targetted function.

https://wiki.multitheftauto.com/wiki/OnClientPreRender

I am not sure if attached is the right word for this example, because it isn't really attached to the event. It is saved in a table which gets looped every frame.


Performance

Is this code bad for performance? The answer to that is NO.

I ran a test for it, and it seems that addRenderEvent used less CPU AFTER adding the events.  (addRenderEvent: 31% CPU, addEventHandler 99/100% CPU) Adding the event will probably use more CPU, but that is only one execution. Feel free to re-test this example, I am interested how it would perform on your pc's.

 

Performance test code (Not the source code :cat:)

Spoiler

addCommandHandler("runFirstTest",
function ()
	
	for i=1, 10000 do
		local testFunction = function () end
		addRenderEvent(testFunction)
	end
end)

addCommandHandler("runSecondTest",
function ()
	for i=1, 10000 do
		local testFunction = function () end
		addEventHandler("onClientRender", root, testFunction)
	end
end)

 

 

 


Source code:

Spoiler

     
------------------------------
-- Authors: IIYAMA and Kenix --
------------------------------

local addEventHandler 		= addEventHandler;
local removeEventHandler 	= removeEventHandler;
local table_remove			= table.remove;
local unpack				= unpack;
local len					= table.getn;
local root					= root;

local renderEvents = {
	"onClientRender",
	"onClientPreRender",
	"onClientHUDRender"
}

local allTargetFunctions = {} -- All attached functions will be stored in this table.

local acceptedRenderEventTypes = {} -- Is type in use? See: renderEvents.
local renderEventTypeStatus = {} -- Is the event in use? (so it doesn't have to be attached again)

do
	-- prepare the data
	for i=1, len( renderEvents ) do
		local event = renderEvents[i]
		allTargetFunctions[event] = {}
		acceptedRenderEventTypes[event] = true
		renderEventTypeStatus[event] = false
	end
end


-- render all events here
local processTargetFunction = function ( timeSlice )
	local targetFunctions = allTargetFunctions[ eventName ]
	
	local i = 1
	local itemCount = len(targetFunctions)
	
	repeat
		if targetFunctions[ i ] == true then -- remove = true
			table_remove( targetFunctions, i )
			itemCount = itemCount - 1
		else
			local targetFunctionData = targetFunctions[i]
			local arguments = targetFunctionData[2]
			
			if not arguments then
				targetFunctionData[ 1 ]( timeSlice )
			else
				if timeSlice then
					targetFunctionData[ 1 ]( timeSlice, unpack( arguments ) )
				else
					targetFunctionData[ 1 ]( unpack( arguments ) )
				end
			end
			
			i = i + 1
		end
	until i > itemCount
end


-- check if a function is already attached
function isRenderEventAdded (theFunction, event)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	
	local targetFunctions = allTargetFunctions[event]
	
	for i = 1, len( targetFunctions ) do
		if targetFunctions[i] and targetFunctions[i] ~= true and targetFunctions[i][1] == theFunction then
			return true
		end
	end
	
	return false
end

local isRenderEventAdded = isRenderEventAdded

-- add render event, default type is onClientRender
function addRenderEvent(theFunction, event, ...)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	
	if not isRenderEventAdded(theFunction) then
		local targetFunctions = allTargetFunctions[event]
		
		-- Don't pass an arguments if it not needed.
		local aArgs = { ... };
		local mArgs = len( aArgs ) > 0 and aArgs or nil;
		
		targetFunctions[ len( targetFunctions ) + 1 ] = { theFunction, mArgs }
		
		-- attach an event
		if not renderEventTypeStatus[event] then
			addEventHandler (event, root, processTargetFunction, false, "high")
			renderEventTypeStatus[event] = true
		end
		
		return true
	end
	
	return false
end

-- remove a render event
function removeRenderEvent(theFunction, event)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	
	local targetFunctions = allTargetFunctions[event]
	
	for i = 1, len( targetFunctions ) do
		if targetFunctions[i] and targetFunctions[i] ~= true and targetFunctions[i][1] == theFunction then
			targetFunctions[i] = true -- true = remove
			
			if len( targetFunctions ) == 0 then
				if renderEventTypeStatus[event] then
					removeEventHandler (event, root, processTargetFunction)
					renderEventTypeStatus[event] = false
				end
			end
			
			return true
		end
	end
	
	return false
end

Updated by @Kenix

Repository: https://github.com/Kenix157/mta_render_events

 

Previous version:

Spoiler


--------------------
-- Author: IIYAMA --
--------------------

local renderEvents = {
	"onClientRender",
	"onClientPreRender",
	"onClientHUDRender"
}

local allTargetFunctions = {} -- All attached functions will be stored in this table.

local acceptedRenderEventTypes = {} -- Is type in use? See: renderEvents.
local renderEventTypeStatus = {} -- Is the event in use? (so it doesn't have to be attached again)




do
	-- prepare the data
	for i=1, #renderEvents do
		local event = renderEvents[i]
		allTargetFunctions[event] = {}
		acceptedRenderEventTypes[event] = true
		renderEventTypeStatus[event] = false
	end
end

-- render all events here
local processTargetFunction = function (timeSlice)
	local targetFunctions = allTargetFunctions[eventName]
	for i=#targetFunctions, 1, -1  do
		local targetFunctionData = targetFunctions[i]
		local arguments = targetFunctionData[2]
		if not arguments then
			targetFunctionData[1](timeSlice)
		else
			if timeSlice then
				targetFunctionData[1](timeSlice, unpack(arguments))
			else
				targetFunctionData[1](unpack(arguments))
			end
		end
	end
end



-- check if a function is already attached
local checkIfFunctionIsTargetted = function (theFunction, event)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	local targetFunctions = allTargetFunctions[event]
	for i=1, #targetFunctions do
		if targetFunctions[i][1] == theFunction then
			return true
		end
	end
	return false
end

-- add render event, default type is onClientRender
function addRenderEvent(theFunction, event, ...)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	if not checkIfFunctionIsTargetted(theFunction) then
		local targetFunctions = allTargetFunctions[event]
		targetFunctions[#targetFunctions + 1] = {theFunction, {...}}
		
		-- attach an event
		if not renderEventTypeStatus[event] then
			addEventHandler (event, root, processTargetFunction, false, "high")
			renderEventTypeStatus[event] = true
		end
		return true
	end
	return false
end

-- remove a render event
function removeRenderEvent(theFunction, event)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	local targetFunctions = allTargetFunctions[event]
	for i=1, #targetFunctions do
		if targetFunctions[i][1] == theFunction then
			table.remove(targetFunctions, i)
			if #targetFunctions == 0 then
				if renderEventTypeStatus[event] then
					removeEventHandler (event, root, processTargetFunction)
					renderEventTypeStatus[event] = false
				end
			end
			return true
		end
	end
	return false
end

 

 

 

 

 

Edited by IIYAMA
  • Like 2
  • Thanks 8

Share this post


Link to post

Excellent. This is a very nice and clean implementation. I hope many people start using this.

  • Like 1

Share this post


Link to post
On 06/03/2019 at 22:00, Kenix said:

You can also add this in top of the script, it should increase performance.

 


local unpack = unpack;
local len = table.getn; -- instead of #, you can't # make as local variable.


Here is an updated version:
https://github.com/Kenix157/mta_render_events

Nice work!

 

I updated the code in the tutorial.

Share this post


Link to post
Posted (edited)

I noticed after applying Kenix his update that 1 of the loops wasn't inverted. Which is important for removing addRenderEvent's within the same process.

 

 

	-- for i = 1, len( targetFunctions ) do -- old
    for i = len( targetFunctions ), 1, -1 do

 

Just like with domino, when you take a domino out of the line, it will not work well...

Edited by IIYAMA

Share this post


Link to post
On 16/05/2019 at 22:17, IIYAMA said:

I noticed after applying Kenix his update that 1 of the loops wasn't inverted. Which is important for removing addRenderEvent's within the same process.

 

 


	-- for i = 1, len( targetFunctions ) do -- old    for i = len( targetFunctions ), 1, -1 do

 

Just like with domino, when you take a domino out of the line, it will not work well...

In removeRenderEvent function?

  • Like 1

Share this post


Link to post

 

 

 

If the loop isn't invert, this will not work well:

-- This will go wrong
function theFunction () -- first function
  removeRenderEvent(theFunction)
end

addRenderEvent(theFunction)


-- add multiple functions, else the context isn't valid
for i=1, 9 do
  addRenderEvent(function () end)
end

 

functions:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

 

Looping: from 1 t/m 10

| = index

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

 

Deleting 1 during the loop:

|

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

 

Going on:

   | > > > > > > > >

2, 3, 4, 5, 6, 7, 8, 9, 10, nothing

 

 

error:

                                  |

2, 3, 4, 5, 6, 7, 8, 9, 10, nothing

 

If the loop is inverted, the loop will not have problems when items are removed.

 

 

 

 

 

 

  • Like 1

Share this post


Link to post
Posted (edited)
On 19/05/2019 at 14:24, IIYAMA said:

 

 

 

If the loop isn't invert, this will not work well:


-- This will go wrongfunction theFunction () -- first function  removeRenderEvent(theFunction)endaddRenderEvent(theFunction)-- add multiple functions, else the context isn't validfor i=1, 9 do  addRenderEvent(function () end)end

 

functions:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

 

Looping: from 1 t/m 10

| = index

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

 

Deleting 1 during the loop:

|

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

 

Going on:

   | > > > > > > > >

2, 3, 4, 5, 6, 7, 8, 9, 10, nothing

 

 

error:

                                  |

2, 3, 4, 5, 6, 7, 8, 9, 10, nothing

 

If the loop is inverted, the loop will not have problems when items are removed.

 

 

 

 

 

 



@IIYAMA It cause another problem with render events, it starts call from end (which is not needed to us, because dx render events has "layers" and you need to call from 1 to table lenght)

If you're using table.remove function, everything should work fine after remove item from table.
Let me explain:

local a = {};

local func = function() end;

a[ #a + 1 ] = func;

for i = 1, #a do
 if a[ i ] == func then
  table.remove( a, i );
 end
end

print( #( a ) ); -- 0

table.remove safely remove item from table, it's not contains a [1] index with nil value, it's clear

Proof:

local a = {};

local func = function() end;

a[ #a + 1 ] = func;

for i = 1, #a do
 if a[ i ] == func then
  table.remove( a, i );
 end
end

print( #( a ) ); -- 0

local func = function() end;

a[ #a + 1 ] = func;


print( #( a ) ); -- 1



Another example:
 

local a = { 1, 2 };

local func = function() end;

a[ #a + 1 ] = func;

for i = 1, #a do
 if a[ i ] == func then
  table.remove( a, i );
 end
end

print( #( a ) ); -- 2

local func = function() end;

a[ #a + 1 ] = func;

for i = 1, #a do
 if a[ i ] == func then
  table.remove( a, i );
 end
end

print( #( a ) ); -- 2

And finally (shows how another items is reordered): from { [1] = 1, [2] = func, [3] = 2} -> {[1] = 1, [2] = 2}
 

local func = function() end;

local a = { 1, func, 2 };

print( "before", #a ); -- 3

for i = 1, #a do
 if a[ i ] == func then
  table.remove( a, i );
 end
end

print( "after", #a ); -- 2

 

test here: https://www.Lua.org/cgi-bin/demo

Edited by Kenix

Share this post


Link to post
49 minutes ago, Kenix said:

It cause another problem with render events, it starts call from end

Agree, didn't thought about that one, but the counting up loop would still have the issue of skipping one(or more > context) of the items for that frame. The loop doesn't take into account that the index should stop increasing when an item is removed, as the array is collapsing.

 

 

Would this not cover all the issues?

local i = 1
repeat 
	if a[ i ] == func then
		table.remove( a, i )
	else
		i = i + 1
	end
until not a[ i ]

 

 

 

 

Share this post


Link to post
3 hours ago, IIYAMA said:

Agree, didn't thought about that one, but the counting up loop would still have the issue of skipping one(or more > context) of the items for that frame. The loop doesn't take into account that the index should stop increasing when an item is removed, as the array is collapsing.

 

 

Would this not cover all the issues?


local i = 1repeat 	if a[ i ] == func then		table.remove( a, i )	else		i = i + 1	enduntil not a[ i ]

 

 

 

 

I think it's good solution. Code updated.

Share this post


Link to post
7 hours ago, Kenix said:

I think it's good solution. Code updated.

It should be applied on the render function.

The removeRenderEvent is fine as it was.

 

 

The problem:

 

<New frame>

Queue start < fix should be applied here

Func 1

removeRenderEvent (in func 1)

Func 2 (error)

Queue end

 

Share this post


Link to post
On 19/06/2019 at 12:30, IIYAMA said:

It should be applied on the render function.

The removeRenderEvent is fine as it was.

 

 

The problem:

 

<New frame>

Queue start < fix should be applied here

Func 1

removeRenderEvent (in func 1)

Func 2 (error)

Queue end

 

This problem still exist? 

Share this post


Link to post
1 hour ago, majqq said:

This problem still exist? 

Yes.

I have a definitive solution in mind, but currently not the time to solve it.

  • Like 1

Share this post


Link to post
Posted (edited)

@Kenix

@majqq

 

I think the issue is now solved. Had to make some annoying adjustments, before it finally did work in all the different context/scenario's. Now it doesn't matter when and where an item gets remove at any index. When it is remove, it gets temporary replaced by a boolean and removed while iterating/rendering. The render order is correct as well. I know it is a bit of a dirty hack, but it does not :scrambleup: things up as it is in some of the known scenario's.

 

Let me know if there are no new issues occurring.

 

New version: (need some more testing, just in case)

Spoiler

     
------------------------------
-- Authors: IIYAMA and Kenix --
------------------------------

local addEventHandler 		= addEventHandler;
local removeEventHandler 	= removeEventHandler;
local table_remove			= table.remove;
local unpack				= unpack;
local len					= table.getn;
local root					= root;

local renderEvents = {
	"onClientRender",
	"onClientPreRender",
	"onClientHUDRender"
}

local allTargetFunctions = {} -- All attached functions will be stored in this table.

local acceptedRenderEventTypes = {} -- Is type in use? See: renderEvents.
local renderEventTypeStatus = {} -- Is the event in use? (so it doesn't have to be attached again)

do
	-- prepare the data
	for i=1, len( renderEvents ) do
		local event = renderEvents[i]
		allTargetFunctions[event] = {}
		acceptedRenderEventTypes[event] = true
		renderEventTypeStatus[event] = false
	end
end


-- render all events here
local processTargetFunction = function ( timeSlice )
	local targetFunctions = allTargetFunctions[ eventName ]
	
	local i = 1
	local itemCount = len(targetFunctions)
	
	repeat
		if targetFunctions[ i ] == true then -- remove = true
			table_remove( targetFunctions, i )
			itemCount = itemCount - 1
		else
			local targetFunctionData = targetFunctions[i]
			local arguments = targetFunctionData[2]
			
			if not arguments then
				targetFunctionData[ 1 ]( timeSlice )
			else
				if timeSlice then
					targetFunctionData[ 1 ]( timeSlice, unpack( arguments ) )
				else
					targetFunctionData[ 1 ]( unpack( arguments ) )
				end
			end
			
			i = i + 1
		end
	until i > itemCount
end


-- check if a function is already attached
function isRenderEventAdded (theFunction, event)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	
	local targetFunctions = allTargetFunctions[event]
	
	for i = 1, len( targetFunctions ) do
		if targetFunctions[i] and targetFunctions[i] ~= true and targetFunctions[i][1] == theFunction then
			return true
		end
	end
	
	return false
end

local isRenderEventAdded = isRenderEventAdded

-- add render event, default type is onClientRender
function addRenderEvent(theFunction, event, ...)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	
	if not isRenderEventAdded(theFunction) then
		local targetFunctions = allTargetFunctions[event]
		
		-- Don't pass an arguments if it not needed.
		local aArgs = { ... };
		local mArgs = len( aArgs ) > 0 and aArgs or nil;
		
		targetFunctions[ len( targetFunctions ) + 1 ] = { theFunction, mArgs }
		
		-- attach an event
		if not renderEventTypeStatus[event] then
			addEventHandler (event, root, processTargetFunction, false, "high")
			renderEventTypeStatus[event] = true
		end
		
		return true
	end
	
	return false
end

-- remove a render event
function removeRenderEvent(theFunction, event)
	if not event or not acceptedRenderEventTypes[event] then
		event = "onClientRender"
	end
	
	local targetFunctions = allTargetFunctions[event]
	
	for i = 1, len( targetFunctions ) do
		if targetFunctions[i] and targetFunctions[i] ~= true and targetFunctions[i][1] == theFunction then
			targetFunctions[i] = true -- true = remove
			
			if len( targetFunctions ) == 0 then
				if renderEventTypeStatus[event] then
					removeEventHandler (event, root, processTargetFunction)
					renderEventTypeStatus[event] = false
				end
			end
			
			return true
		end
	end
	
	return false
end

 

 

 

Edited by IIYAMA
  • Like 1

Share this post


Link to post
Posted (edited)
3 hours ago, IIYAMA said:

@Kenix

@majqq

 

I think the issue is now solved. Had to make some annoying adjustments, before it finally did work in all the different context/scenario's. Now it doesn't matter when and where an item gets remove at any index. When it is remove, it gets temporary replaced by a boolean and removed while iterating/rendering. The render order is correct as well. I know it is a bit of a dirty hack, but it does not :scrambleup: things up as it is in some of the known scenario's.

 

Let me know if there are no new issues occurring.

 

New version: (need some more testing, just in case)

  Reveal hidden contents


      -------------------------------- Authors: IIYAMA and Kenix --------------------------------local addEventHandler 		= addEventHandler;local removeEventHandler 	= removeEventHandler;local table_remove			= table.remove;local unpack				= unpack;local len					= table.getn;local root					= root;local renderEvents = {	"onClientRender",	"onClientPreRender",	"onClientHUDRender"}local allTargetFunctions = {} -- All attached functions will be stored in this table.local acceptedRenderEventTypes = {} -- Is type in use? See: renderEvents.local renderEventTypeStatus = {} -- Is the event in use? (so it doesn't have to be attached again)do	-- prepare the data	for i=1, len( renderEvents ) do		local event = renderEvents[i]		allTargetFunctions[event] = {}		acceptedRenderEventTypes[event] = true		renderEventTypeStatus[event] = false	endend-- render all events herelocal processTargetFunction = function ( timeSlice )	local targetFunctions = allTargetFunctions[ eventName ]		local i = 1	local itemCount = len(targetFunctions)		repeat		if targetFunctions[ i ] == true then -- remove = true			table_remove( targetFunctions, i )			itemCount = itemCount - 1		else			local targetFunctionData = targetFunctions[i]			local arguments = targetFunctionData[2]						if not arguments then				targetFunctionData[ 1 ]( timeSlice )			else				if timeSlice then					targetFunctionData[ 1 ]( timeSlice, unpack( arguments ) )				else					targetFunctionData[ 1 ]( unpack( arguments ) )				end			end						i = i + 1		end	until i > itemCountend-- check if a function is already attachedfunction isRenderEventAdded (theFunction, event)	if not event or not acceptedRenderEventTypes[event] then		event = "onClientRender"	end		local targetFunctions = allTargetFunctions[event]		for i = 1, len( targetFunctions ) do		if targetFunctions[i] and targetFunctions[i] ~= true and targetFunctions[i][1] == theFunction then			return true		end	end		return falseendlocal isRenderEventAdded = isRenderEventAdded-- add render event, default type is onClientRenderfunction addRenderEvent(theFunction, event, ...)	if not event or not acceptedRenderEventTypes[event] then		event = "onClientRender"	end		if not isRenderEventAdded(theFunction) then		local targetFunctions = allTargetFunctions[event]				-- Don't pass an arguments if it not needed.		local aArgs = { ... };		local mArgs = len( aArgs ) > 0 and aArgs or nil;				targetFunctions[ len( targetFunctions ) + 1 ] = { theFunction, mArgs }				-- attach an event		if not renderEventTypeStatus[event] then			addEventHandler (event, root, processTargetFunction, false, "high")			renderEventTypeStatus[event] = true		end				return true	end		return falseend-- remove a render eventfunction removeRenderEvent(theFunction, event)	if not event or not acceptedRenderEventTypes[event] then		event = "onClientRender"	end		local targetFunctions = allTargetFunctions[event]		for i = 1, len( targetFunctions ) do		if targetFunctions[i] and targetFunctions[i] ~= true and targetFunctions[i][1] == theFunction then			targetFunctions[i] = true -- true = remove						if len( targetFunctions ) == 0 then				if renderEventTypeStatus[event] then					removeEventHandler (event, root, processTargetFunction)					renderEventTypeStatus[event] = false				end			end						return true		end	end		return falseend

 

 

 

Thanks, i will let you know, if something will not work.

Besides, what do does in this case?

L2Ob0f6.png

Edited by majqq

Share this post


Link to post
4 hours ago, majqq said:

Thanks, i will let you know, if something will not work.

Besides, what do does in this case?

L2Ob0f6.png

It is just an extra block, which runs 1 time. Similar to:

if true then
  
end

 

And no it is not required, but it makes it very clear that some action are happening there.

You more often see those be used to enclose reused variables, without overwrite the original.

local base = 1
if 1 == base then
  do
    local base = 2
    print(base) -- 2
  end
  
  do
    local base = 3
    print(base) -- 3
  end
  print(base) -- 1
end

 

Or delete unused variables:

local a
do
  local b = 100
  a = b * b * b
end

 

  • Like 1

Share this post


Link to post

Updated the topic to latest version.

 

P.s. if you are interested in always receive notifications when the newest version comes out. There is a feature on the forum called: following. This allows you to keep track of updates from a selected topic. It is located on the right + top corner of where the topic starts.

  • Like 1

Share this post


Link to post

I find it ridiculous that this isnt a basic thing of Lua itself, it should not eat less performance when functions are stored as local variables...

Share this post


Link to post

 

On 14/08/2019 at 23:27, Einheit-101 said:

I find it ridiculous that this isnt a basic thing of Lua itself, it should not eat less performance when functions are stored as local variables...

It is indeed a little bit strange. But on the other side, it does provide a way to optimise access for variables manually. It is just a pity that it can't optimise itself based on patterns of variable requests.

Share this post


Link to post

I would suggest a "setting" that automatically stores all Lua functions inside the computers RAM for people who can afford that, it would increase performance of large scripts tremendously.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.