Skip to content

Add access to CGameRules and its properties #363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 21, 2020
Merged

Add access to CGameRules and its properties #363

merged 11 commits into from
Dec 21, 2020

Conversation

Ayuto
Copy link
Member

@Ayuto Ayuto commented Nov 15, 2020

Here is a draft as a base for a discussion. My initial plan was to also expose methods from the CGameRules class, but unfortunately the classes (even the abstract(!) classes) contain many unimplemented methods. That's fine for the Windows build, but Linux can't handle that. Thus, I simply created a fake class (CGameRulesWrapper), which provides methods to access properties via SendTables. The alternative would be to add dummy implementations for every missing implementation. But I'm not sure whether the header is up to date. So, that could also cause issues.

Any opinions?

Since the Linux build doesn't like running with opaque types, I had to change back a lot...
@jordanbriere
Copy link
Contributor

What about using the string tables to resolve the class rather than looking for the entity?

from entities import ServerClassGenerator
from stringtables import string_tables

name = string_tables['GameRulesCreation'].get_user_data('classname') + 'Proxy'
for server_class in ServerClassGenerator():
    if server_class.name == name:
        break
else:
    raise Exception(f'{name} class not found.')

for prop in server_class.table:
    if prop.name.endswith('gamerules_data'):
        break
else:
    raise Exception('Data property not found.')

gamerules = prop.data_table_proxy_function(None, None, None, None, 0)

@Ayuto
Copy link
Member Author

Ayuto commented Nov 17, 2020

That's a great idea! Though, we wouldn't be able to access the game rules instance during certain events when the string tables aren't loaded yet, right?

@jordanbriere
Copy link
Contributor

jordanbriere commented Nov 17, 2020

That's a great idea! Though, we wouldn't be able to access the game rules instance during certain events when the string tables aren't loaded yet, right?

The game rules pointer is tied to the world and the earliest you can access it is when the world is spawned and has been pre-cached. Although the game rules entity is created right before, accessing the game rules pointer at that point would returns the previous one which will be reallocated shortly after based on the current team play rules and whatnot or the returned pointer won't be initialized because the initialization happens here. The game rules entity is also created and deleted multiple times during map changes for some reasons. The string table is updated at the same time the pointer is installed so I don't think we can get more reliable than at this very moment:

from entities import ServerClassGenerator
from entities.constants import WORLD_ENTITY_INDEX
from listeners import OnNetworkedEntitySpawned
from stringtables import string_tables

@OnNetworkedEntitySpawned
def on_networked_entity_spawned(entity):
    if entity.index != WORLD_ENTITY_INDEX:
        return

    name = string_tables['GameRulesCreation'].get_user_data('classname') + 'Proxy'
    for server_class in ServerClassGenerator():
        if server_class.name == name:
            break
    else:
        raise Exception(f'{name} class not found.')

    for prop in server_class.table:
        if prop.name.endswith('gamerules_data'):
            break
    else:
        raise Exception('Data property not found.')

    gamerules = prop.data_table_proxy_function(None, None, None, None, 0)
    print('>> Current game rules address:', gamerules.address)

Perhaps maintaining a global pointer from that listener would save us the burden of looking it up every times.

@CookStar
Copy link
Contributor

CookStar commented Nov 18, 2020

If we want to always return a pointer reliably, we can extract it from g_pGameRules.
Of course, we need to use a signature...

from memory import find_binary

#CS:GO Linux
def get_game_rules():
    server = find_binary("server", srv_check=False)
    ptr = server.find_pointer(b"\x55\x89\xE5\x56\x53\x83\xEC\x30\xA1\x2A\x2A\x2A\x2A\x8B\x5D\x08\x85\xC0\x74\x2A", 9, 1)
    if ptr:
        return ptr.get_pointer()
    else:
        raise Exception("game rules are currently unavailable.")

@jordanbriere
Copy link
Contributor

If we want to always return a pointer reliably, we can extract it from g_pGameRules.
Of course, we need to use a signature...

Indeed, it would be the most reliable to an extent but wouldn't worth the maintainability on the long run. If we were to go that route, we could get it from a function we have access to, for example IServerGameDLL::Status so we just have to maintain an offset that we could easily update with this snippet:

from engines.server import server_game_dll
from memory import get_virtual_function
from entities import ServerClassGenerator
from stringtables import string_tables

name = string_tables['GameRulesCreation'].get_user_data('classname') + 'Proxy'
for server_class in ServerClassGenerator():
    if server_class.name == name:
        break
else:
    raise Exception(f'{name} class not found.')

for prop in server_class.table:
    if prop.name.endswith('gamerules_data'):
        break
else:
    raise Exception('Data property not found.')

gamerules = prop.data_table_proxy_function(None, None, None, None, 0)

status = get_virtual_function(server_game_dll, 'Status')

for i in range(2048):
    offset = i * 4
    try:
        if status.get_pointer(offset).get_ulong() == gamerules.address:
            break
    except (RuntimeError, ValueError):
        continue
else:
    raise Exception('offset not found')

print('>> Offset of g_pGameRules:', offset)
print('>> Address of game rules:', status.get_pointer(offset).get_ulong())

Though, we should avoid using signatures/offsets unless we absolutely have to. Another option could be to store the proxy class in our data so that we can resolve the proxy once and just call it whenever we want the pointer. This means we don't have to lookup for an entity, nor a string table, and can just call the proxy which will returns NULL if the game rules are not allocated yet.

@CookStar
Copy link
Contributor

Another option could be to store the proxy class in our data so that we can resolve the proxy once and just call it whenever we want the pointer. This means we don't have to lookup for an entity, nor a string table, and can just call the proxy which will returns NULL if the game rules are not allocated yet.

Indeed, that's the most elegant way!

@Ayuto Ayuto linked an issue Dec 11, 2020 that may be closed by this pull request
@Ayuto
Copy link
Member Author

Ayuto commented Dec 11, 2020

Alright, I have finished my changes :)

from engines.gamerules import find_game_rules

game_rules = find_game_rules()

print('m_bMapHasBombTarget', game_rules.get_property_bool('cs_gamerules_data.m_bMapHasBombTarget'))
print('m_bMapHasRescueZone', game_rules.get_property_bool('cs_gamerules_data.m_bMapHasRescueZone'))

@jordanbriere @CookStar Do you have anything else to add?

@Ayuto Ayuto marked this pull request as ready for review December 11, 2020 23:36
Copy link
Contributor

@jordanbriere jordanbriere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, nice job! Slight note regarding find_game_rules_property_offset though.

@CookStar
Copy link
Contributor

It took me a while to try it out, and I think it all the features work perfectly!

However, there was a compilation error with CS:GO and Blade, so I added a fix. #376

@Ayuto Ayuto deleted the gamerules branch February 6, 2021 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Why was gamerules discarded?
3 participants