PlayerDictionary vs CachedPlayer

Please post any questions about developing your plugin here. Please use the search function before posting!
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 3:11 pm

After L'In20Cible's commits with Cached player instances, an obvious question is brewing. Is there a difference between the two now?

Syntax: Select all

from players.dictionary import PlayerDictionary

PLAYERS = PlayerDictionary()
player = PLAYERS[1]

Syntax: Select all

from players.entity import Player

# Cached by default
player = Player(1)


Edit:
Quick test:

Syntax: Select all

import time

from players.entity import Player
from players.dictionary import PlayerDictionary

PLAYERS = PlayerDictionary()

start = time.time()
for i in range(1000000):
PLAYERS[1]
print('PlayerDictionary(1):', time.time() - start)

start = time.time()
for i in range(1000000):
Player(1)
print('Player(1):', time.time() - start)


Syntax: Select all

PlayerDictionary(1): 0.21706199645996094
Player(1): 0.5515789985656738
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 23, 2019 5:05 pm

The behaviour is basically the same, but as you noted using a PlayerDictionary will be slightly faster. Which is to be expected, since you do not attempt to instantiate an instance but look up a cache directly. You are likely to get similar timing results using:

Syntax: Select all

Player.cache[1]
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 6:33 pm

L'In20Cible wrote:The behaviour is basically the same, but as you noted using a PlayerDictionary will be slightly faster. Which is to be expected, since you do not attempt to instantiate an instance but look up a cache directly. You are likely to get similar timing results using:

Syntax: Select all

Player.cache[1]

it's a little weird don't you think so? I mean why would you do that. If anyone wants a cache he will dive into PlayerDictionary, and if not just Player(), you just created synonym of PlayerDictionary.
And if i prefer PlayerDictionary, does it mean that i will have two cached dictionary at the same time?
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 23, 2019 7:16 pm

InvisibleSoldiers wrote:it's a little weird don't you think so? I mean why would you do that.

I simply provided an example that came closer to your first snippet; simple retrieval of a key from a dictionary.

InvisibleSoldiers wrote:If anyone wants a cache he will dive into PlayerDictionary, and if not just Player(), you just created synonym of PlayerDictionary.

Both have very distinct use cases. PlayerDictionary is a container you have control over its content, while the other is an internal caching system. For example, if a plugin wants a Player instance from a hook he would do:

Syntax: Select all

player = make_object(Player, stack[0])


This is where the internal cache comes into play. Before, it would just instantiate a new Player every call, now it retrieves the instance from the internal cache and thus makes all future call to that function faster than it was.

InvisibleSoldiers wrote:And if i prefer PlayerDictionary, does it mean that i will have two cached dictionary at the same time?
It depend how you declare your dictionary. If you do not want to use the internal cache, or have the instances you created internally cached, you need to specify it explicitly to your factory class. E.g.

Syntax: Select all

players = PlayerDictionary(caching=False)


Which would make your instances private, and not cached into Player.cache whatsoever.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 7:58 pm

L'In20Cible wrote:
InvisibleSoldiers wrote:

Syntax: Select all

player = make_object(Player, stack[0])


Syntax: Select all

player = PLAYERS[index_from_pointer(stack[0])]
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 23, 2019 8:16 pm

InvisibleSoldiers wrote:
L'In20Cible wrote:
InvisibleSoldiers wrote:

Syntax: Select all

player = make_object(Player, stack[0])


Syntax: Select all

player = PLAYERS[index_from_pointer(stack[0])]

If that is what you want to do, then go for it. I personally wouldn't bother writing extra code to save 0.0000002 ms per call, for the same reason I wouldn't drive extra miles to save 0.0000002 cents for a cup of coffee. The real important difference here is that Player(1) is like 10 times faster than Player(1, caching=False). Either way, you don't seem to understand that the goal was to optimize existing codes, and I think you start to becomes a little obsessed to be honest. :tongue:
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 8:37 pm

I guess no one won't use PlayerDictionary with default base, and the optimizations from you are not incomprehensible, because you are trying to make caching available to people who may not have wanted it or those who are just stupid.
L'In20Cible wrote:What is a good tone designing programs?

Not to create aliases for existing classes.
Last edited by InvisibleSoldiers on Mon Dec 23, 2019 9:06 pm, 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 23, 2019 9:01 pm

InvisibleSoldiers wrote:Do you want write it?

Syntax: Select all

player = PLAYERS[make_object(Player, stack[0]).index]

If you have a self player class with custom attributes.
take a base object to take an index from there to take an object that is built on the base

To use the internal cache with a custom class, you could simply do:

Syntax: Select all

player = make_object(MyPlayer, stack[0])

But yeah, if you have a PlayerDictionary then the ideal route would be what you posted above.

InvisibleSoldiers wrote:I was just wondering what I should use in my next plugin and thought about a good tone designing programs and it hardly matches it.
What is a good tone designing programs?
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 9:07 pm

I guess no one won't use PlayerDictionary with the default base, and the optimizations from you are not incomprehensible, because you are trying to make caching available for people who may not have wanted it or those who are just stupid.
L'In20Cible wrote:What is a good tone designing programs?

Not to create aliases for existing classes.
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 23, 2019 9:20 pm

InvisibleSoldiers wrote:I guess no one won't use PlayerDictionary with default base, and the optimizations from you are not incomprehensible, because you are trying to make caching available for people who may not have wanted it or those who are just stupid.
L'In20Cible wrote:What is a good tone designing programs?

Not to create aliases for existing classes.
Again, they are not aliases and have totally different use cases. The fact you are using a PlayerDictionary as a cache, doesn't means this is its main purpose. This is a container you can use to keep track of instances while not having to clean it up yourself. For example, look at my Parachute plugin, I use an EntityDictionary to keep track of the parachute I create, and since I use an EntityDictionary instead of a regular dictionary, I do not have to manually listen for entity deletion in order to update my dictionary; this is done by the EntityDictionary directly.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 9:40 pm

L'In20Cible wrote:Again, they are not aliases and have totally different use cases. The fact you are using a PlayerDictionary as a cache, doesn't means this is its main purpose. This is a container you can use to keep track of instances while not having to clean it up yourself. For example, look at my Parachute plugin, I use an EntityDictionary to keep track of the parachute I create, and since I use an EntityDictionary instead of a regular dictionary, I do not have to manually listen for entity deletion in order to update my dictionary; this is done by the EntityDictionary directly.

Purpose of the EntityDictionary class to store entity objects. Even the class name talks about it. EntityDictionary. Why you didn't put the parachute in custom entity class to realize main purpose of EntityDictionary? Assume that you thought that one instance attribute doesn't deserve a creating custom class for it. Why? Was it really worth it to distort the purpose of EntityDictionary? Are you too lazy to add listener @OnPlayerDisconnect or something similar if you preferred to store a entity variable in a dictionary?
And it is main purpose of EntityDictionary, the rest is your imagination, even Wikipedia says
Helper class used to store entity instances
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 23, 2019 10:13 pm

InvisibleSoldiers wrote:Purpose of the EntityDictionary class to store entity objects. Even the class name talks about it. EntityDictionary.
Look carefully:

Syntax: Select all

parachute = Entity.create('prop_dynamic_override')
parachutes[parachute.index] = parachute

The first line creates an entity, then the second line stores its instance in the dictionary. This is exactly the purpose of that class; storing entity instances.
InvisibleSoldiers wrote:Why you didn't put the parachute in custom entity class to realize main purpose of EntityDictionary? Assume that you thought that one instance attribute doesn't deserve a creating custom class for it.

I'm not defining any variables or methods, so why would I do that? There is no reason to subclass a class unless you intend to extend it.
InvisibleSoldiers wrote:Are you too lazy to add listener @OnPlayerDisconnect or something similar if you preferred to store a entity variable in a dictionary?

Nothing to do with being lazy. There is absolutely no reason to reinvent the wheel over using a class specifically designed to take care of that for me.
InvisibleSoldiers wrote:And it is main purpose of EntityDictionary, the rest is your imagination, even Wikipedia says
Helper class used to store entity instances
Well, I wrote that very sentence so I guess I'm aware of it but thanks for the reminder!

Anyways, I'm not sure why this turned into such hostilities while I thought we were having a constructive discussion. So yeah, I'm out; happy holidays!
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Mon Dec 23, 2019 10:31 pm

OK, i read carelessly, but then why you didn't link 'prop_dynamic_override' entity with a custom player object in this way:

Syntax: Select all

Player.parachute = Entity.create('prop_dynamic_override')

It's not sublass.
And why do you consider defending self point of view as hostility?
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 23, 2019 10:54 pm

InvisibleSoldiers wrote:OK, i read carelessly, but then why you didn't link 'prop_dynamic_override' entity with a custom player object in this way:

Syntax: Select all

Player.parachute = Entity.create('prop_dynamic_override')

It's not sublass.


How would I know Player.parachute is still a valid entity when I use it later? I would need to validate it every time I use it, which would not only be redundant, but could be undefined. The Entity class is just a proxy to an entity matching an index, if that index has been freed, you crash. If that index has been freed then re-used by a different entity, then you now interact with a totally different entity. True that I could listen to entity deletions, loop through all players, determine if the deleted entity is their parachute in order to invalidate it, etc. but that would also be redundant.

InvisibleSoldiers wrote:And why do you consider defending self point of view as hostility?
Well, I'm all for debating and having a constructive discussion, but when a party starts implying the other is lazy and imagining things this sure sounds aggressive and hostile argument soon to escalate.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Tue Dec 24, 2019 2:39 am

L'In20Cible wrote:Again, they are not aliases and have totally different use cases. The fact you are using a PlayerDictionary as a cache, doesn't means this is its main purpose. This is a container you can use to keep track of instances while not having to clean it up yourself. For example, look at my Parachute plugin, I use an EntityDictionary to keep track of the parachute I create, and since I use an EntityDictionary instead of a regular dictionary, I do not have to manually listen for entity deletion in order to update my dictionary; this is done by the EntityDictionary directly.

Referring to this post, now you can do the same with Entity.cache[index], isn't it? So, it's alias.
L'In20Cible wrote:How would I know Player.parachute is still a valid entity when I use it later? I would need to validate it every time I use it, which would not only be redundant, but could be undefined. The Entity class is just a proxy to an entity matching an index, if that index has been freed, you crash. If that index has been freed then re-used by a different entity, then you now interact with a totally different entity. True that I could listen to entity deletions, loop through all players, determine if the deleted entity is their parachute in order to invalidate it, etc. but that would also be redundant.

And i swear, you can do it through linking the parachute entity inside self player class and that would be cleaner, but i don't quite understand the line

Syntax: Select all

player.delay(0, parachute_check, (player,))

Why do you create the parachute in a next tick and not in current one? Oh, i swear it can be rewrited in better way.
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 3:37 am

InvisibleSoldiers wrote:Referring to this post, now you can do the same with Entity.cache[index], isn't it? So, it's alias.

No, this is not an alias. Entity.cache is nothing more than the internal cache of the Entity class. Take the following example:

Syntax: Select all

from entities.constants import WORLD_ENTITY_INDEX
from entities.entity import Entity

class MyEntity(Entity):
pass

MyEntity.cache[WORLD_ENTITY_INDEX]

This would produces:

Code: Select all

KeyError: 0

Because that entity was never explicitly instantiated and thus was never cached. While an EntityDictionary would make the instance for you using the given factory class.

InvisibleSoldiers wrote:And i swear, you can do it through linking the parachute entity inside self player class and that would be cleaner,

Yes, it could. But as stated above, would be redundant because it would need to be validated every times it is being used. For example, load the following code, type "cache" then type "whatever" later:

Syntax: Select all

from entities.entity import Entity
from events import Event
from players.entity import Player
from players.dictionary import PlayerDictionary

players = PlayerDictionary()

@Event('player_say')
def player_say(game_event):
text = game_event.get_string('text')
player = players.from_userid(game_event.get_int('userid'))
if text == 'cache':
player.parachute = Entity.create('prop_dynamic_override')
player.parachute.remove()
else:
player.parachute.set_parent(player)

You will crash on linux, and get a runtime error on windows because that entity was removed by the time we used it.

InvisibleSoldiers wrote:but i don't quite understand the line

Syntax: Select all

player.delay(0, parachute_check, (player,))

Why do you create the parachute in a next tick and not in current one? Oh, i swear it can be rewrited in better way.

If you read the thread, you will see it was added as a workaround for issue #157 which wasn't fixed at the time I wrote this plugin.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Tue Dec 24, 2019 4:37 am

Rewrote the plugin with given the fact that a parachute can be removed by a third-party way, that is, not this plugin, but someone else's or something else, that is, if I delete a parachute in this plugin, the error theoretically can not be because i can set a player's parachute again to None. I didn't test it, but seem that everything will work in good manner.I hope that conveyed the main idea which wanted to say.
You can update it to 'Parachute_v0.04' if you wish :grin:

Syntax: Select all

import math

from listeners import OnPlayerRunCommand
from players.entity import Player
from players.dictionary import PlayerDictionary
from players.constants import PlayerButtons
from players.constants import PlayerStates
from core import SOURCE_ENGINE
from engines.precache import Model
from entities.constants import MoveType
from entities.entity import Entity
from paths import GAME_PATH
from stringtables.downloads import Downloadables
from entities.helpers import index_from_inthandle

class ParachutePlayerDictionary(PlayerDictionary):
def on_automatically_removed(self, index):
self[index].close_parachute()

class ParachutePlayer(Player):
def __init__(self, index):
super().__init__(index)

self.parachute = None

def create_parachute(self):
self.parachute = Entity.create('prop_dynamic_override')
self.parachute.model = PARACHUTE_MODEL
self.parachute.owner_handle = self.inthandle
self.parachute.origin = self.origin
self.parachute.model_scale = 0.7
self.target_name = 'parachute'

self.parachute.spawn()

def process_parachute(self, user_cmd):
pitch = user_cmd.view_angles.y

if user_cmd.side_move != 0 and STEERING_LINES_SPEED > 1:
if side_move < 0: pitch += 90.0
else: pitch -= 90.0

pitch = math.radians(pitch)
velocity = Vector(
math.cos(pitch) * STEERING_LINES_SPEED,
math.sin(pitch) * STEERING_LINES_SPEED,
self.fall_velocity * -(FALLING_SPEED * STEERING_LINES_PENALTY)
)
else: velocity = Vector(0.0, 0.0, self.fall_velocity - FALLING_SPEED)

self.base_velocity = velocity

if self.weapon is not None:
if self.parachute.parent != self.weapon:
self.parachute.parent = self.weapon
origin = self.origin
else:
self.parachute.parent = None
view_offset = self.view_offset.copy()
view_offset.z /= 2
origin = self.origin + view_offset

self.parachute.origin = origin
self.parachute.angles = Vector(0.0, pitch, 0.0)

if self.parachute.model_scale < 1.0:
self.parachute.model_scale += 0.024

def close_parachute(self):
self.parachute.remove()

PLAYERS = ParachutePlayerDictionary(ParachutePlayer)
FALLING_SPEED = 32
STEERING_LINES_SPEED = 5
STEERING_LINES_PENALTY = 1.5
DOWNLOADABLES = Downloadables()
DOWNLOADABLES.add_directory(f'models/parachute/{SOURCE_ENGINE}')
DOWNLOADABLES.add_directory('materials/models/parachute')
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')

@OnPlayerRunCommand
def on_player_run_command(player, user_cmd):
if player.is_fake_client(): return

player = PLAYERS[player.index]
if user_cmd.buttons & PlayerButtons.SPEED:
if (player.dead or player.move_type != MoveType.WALK or player.water_level >= 2 or player.fall_velocity < 1.0) and player.parachute is not None:
player.close_parachute()
else:
if player.parachute is None:
player.create_parachute()
player.process_parachute(user_cmd)
elif player.parachute is not None: player.close_parachute()

# If a parachute entity was deleted manually (not in the plugin) that happens very rarely if ever.
# Hope that that will call after player.close_parachute() in same tick.
@OnEntityDeleted
def on_entity_deleted(base_entity):
if base_entity.target_name == 'parachute':
# If CBasePlayer entity was deleted before him parachute. Probably the parachute entity always will be deleted first and the try, except blocks aren't necessary.
try:
index = index_from_inthandle(base_entity.owner_handle)
except ValueError:
pass
else:
player = PLAYERS[index]
player.parachute = None


Edit:
Remove parachute if +speed button wasn't pressed.
By the way, not fully understood the concept of the steering system, it would not be better to use mouse delta from usercmd to determine the acceleration value.
Last edited by InvisibleSoldiers on Tue Dec 24, 2019 5:47 am, edited 5 times in total.
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Tue Dec 24, 2019 4:50 am

L'In20Cible wrote:

Syntax: Select all

from entities.entity import Entity
from events import Event
from players.entity import Player
from players.dictionary import PlayerDictionary

players = PlayerDictionary()

@Event('player_say')
def player_say(game_event):
text = game_event.get_string('text')
player = players.from_userid(game_event.get_int('userid'))
if text == 'cache':
player.parachute = Entity.create('prop_dynamic_override')
player.parachute.remove()
else:
player.parachute.set_parent(player)

You will crash on linux, and get a runtime error on windows because that entity was removed by the time we used it.

Why did you deliberately make a plugin that will crash?

Syntax: Select all

from entities.entity import Entity
from events import Event
from players.entity import Player
from players.dictionary import PlayerDictionary

players = PlayerDictionary()

@Event('player_say')
def player_say(game_event):
text = game_event.get_string('text')
player = players.from_userid(game_event.get_int('userid'))
if text == 'cache':
player.parachute = Entity.create('prop_dynamic_override')
player.parachute.remove()
player.parachute = None
elif player.parachute is not None:
player.parachute.set_parent(player)
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:55 am

InvisibleSoldiers wrote:Rewrote the plugin with given the fact that a parachute can be removed by a third-party way, that is, not this plugin, but someone else's or something else, that is, if I delete a parachute in this plugin, the error theoretically can not be because i can set a player's parachute again to None. I didn't test it, but seem that everything will work in good manner.I hope that conveyed the main idea which wanted to say.

I knew exactly what you meant, and I actually addressed that design above. Unnecessary subclasses doesn't make a code better; it really just pollutes it. Without mentioning that your code introduces a lot of redundant calls, retrieves the same dynamic attributes multiple times, etc. This just adds unnecessary layers in a listener that is extremely noisy. There is a reason why everything is mostly done there; local assignments that can be reused following a specific logic. Your code really adds nothing, nor improve anything, but really just makes it harder to follow by packing everything.

So yeah, thanks for the offer, but I will pass. :smile:
InvisibleSoldiers
Senior Member
Posts: 114
Joined: Fri Mar 15, 2019 6:08 am

Re: PlayerDictionary vs CachedPlayer

Postby InvisibleSoldiers » Tue Dec 24, 2019 6:15 am

L'In20Cible wrote:
InvisibleSoldiers wrote:Rewrote the plugin with given the fact that a parachute can be removed by a third-party way, that is, not this plugin, but someone else's or something else, that is, if I delete a parachute in this plugin, the error theoretically can not be because i can set a player's parachute again to None. I didn't test it, but seem that everything will work in good manner.I hope that conveyed the main idea which wanted to say.

I knew exactly what you meant, and I actually addressed that design above. Unnecessary subclasses doesn't make a code better; it really just pollutes it. Without mentioning that your code introduces a lot of redundant calls, retrieves the same dynamic attributes multiple times, etc. This just adds unnecessary layers in a listener that is extremely noisy. There is a reason why everything is mostly done there; local assignments that can be reused following a specific logic. Your code really adds nothing, nor improve anything, but really just makes it harder to follow by packing everything.

So yeah, thanks for the offer, but I will pass. :smile:

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. 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. 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.

Return to “Plugin Development Support”

Who is online

Users browsing this forum: No registered users and 39 guests