Jump to content

How to modify screen functions?


Recommended Posts

I'm looking at playerstatusscreen.lua and would like to modify playerListing.ban:SetOnClick's inline function (located on line line 562). How could I go about doing this?

For context this is where said function is located:

playerstatusscreen.lua (file)
   PlayerStatusScreen:DoInit (function)
      listingConstructor (local function)
         playerListing.ban:SetOnClick (which gets passed in an inline function)

Link to comment
Share on other sites

I have no idea how to alter screens. You'll have to figure that one out. Once you have access to it, though, you can extend the DoInit function, and make changes after running the original function. Within this extension, so after calling the original function in your extension, as far as I can see, all the playerlistings will have ended up in the self.player_widgets table, so you can run through those and change the onclick function of the ban-button like this:

-- To completely overwrite the onclick function, do this.
for i,v in ipairs(self.player_widgets) do
	v.ban:SetOnClick( function()
		-- I left the original code in here.
		-- Make your changes.
		if playerListing.userid then
			TheFrontEnd:PopScreen()
			UserCommands.RunUserCommand("ban", {user=playerListing.userid}, self.owner)
		end
	end)
end


-- To extend the onclick function, do this.
for i,v in ipairs(self.player_widgets) do
	local oldOnClick = v.ban.onclick
	v.ban:SetOnClick( function()
		-- Do your own stuff
		-- Call the original function.
		oldOnClick()
		-- Do your own stuff
	end)
end

NOTE! This code DOES NOT include the code to access screens OR extend the DoInit function. This code is what you can use in your extension of the DoInit function.

Link to comment
Share on other sites

@L. Ringmaster

Screens are nothing special, they're just like other classes. Global Positions modifies the PlayerStatusScreen in order to add a hide/show location button. In general, modifying a local function somewhere is hard, I do not recommend attempting this (although it's possible with the debug library's upvalues).

In this case, however, it's not just a local function, it gets attached to something that you can then access (self.player_widgets.ban.onclick).

I'm not 100% sure how to write the code for this without digging deeper, but it looks like you may need to hook into some other function to make sure you catch all playerListing objects when/if they get updated. The Global Positions example I linked shows something similar, hooking into the scroll_list updatefn. At a glance it doesn't look like you need to do that, though.

Link to comment
Share on other sites

@rezecib @Ultroman

Pardon my ignorance, but we all start somewhere:

  1. How am I supposed to get a specific user's object using their userid?
    I tried using 
    GetPlayer(playerListing.userid)
    but it noted that the GetPlayer function is deprecated and I should be using ThePlayer instead, however ThePlayer seems to be specific to my character.
    I found i can index into AllPlayers. Do I really need to iterate through AllPlayers to find the one with the matching userid?  
    For context I am trying to use the loop:
    for i,playerListing in pairs(self.scroll_list.static_widgets) do

    And then use the playerListing to access the player's components table, such as one can do using the following:

    ThePlayer.components

     

  2. Is there any way I can get additional information regarding the various parameter inputs to the functions I've found?
    I found I can get the names of members of an object by iterating over and printing the object's name/value pairs:

    for i,v in pairs(ThePlayer) do
        print(i,v) 
    end

    This leaves my knowing what the functions are, but no how to use them. Thus far I've been grepping through data bundles and various mods looking for areas which make use of the function, but that tends to be pretty hit or miss. There's got to be a better way!
     

Link to comment
Share on other sites

14 minutes ago, L. Ringmaster said:

@rezecib @Ultroman

Do I really need to iterate through AllPlayers to find the one with the matching userid?

I think so, yes.

Is there any way I can get additional information regarding the various parameter inputs to the functions I've found?
Not really. Look at the function code. Discern what it does. Find places that use the function, and see what they pass into the parameters. Try to mess with it using best practices of modding (no overwriting game files, and extend functions if you can, instead of overwriting them), and you'll figure it out :) Try to think of a way to do it and post your proposed solution so we can see what you're thinking, and we can help you along. As I said, this screen-stuff is all new ground for me, as well. Rezecib got you off to a good start.

From what I'm reading, you're doing great for someone inexperienced ;) 
 

 

Link to comment
Share on other sites

@rezecib @Ultroman

Thanks so much for your help! I've now completed my first mod!
Please take a look and critique it for me. Let me know if there's anything I could have done in an easier or better way.

Even though it's only 20 lines I spent probably 4-5 hours figuring everything out.

https://steamcommunity.com/sharedfiles/filedetails/?id=1725443110

Link to comment
Share on other sites

@rezecib @Ultroman

I've noticed the mod doesn't seem to work on servers with caves. player.components.inventory:DropEverything() fails as inventory evaluates to nil, however manually executing AllPlayers[n].components.inventory:DropEverything() in console when connected to the server causes the intended behavior.

I've been struggling to troubleshoot the cause due to the following issues I experience when running with caves:

  1. I can't get console output to work properly on servers with caves. When manually entering commands into console I've I've found that only the "print" function's output shows up in the console, and it only does so when executed in "local" mode. Entering commands such as "c_spawn(...)" or "c_godmode()" has the intended effect, but no output is sent to console like usual.
  2. When running with caves, output sent to console with the print commands does not show up in any of my client or server logs. As a workaround I've found that output sent to TheNet:SystemMessage does show up in my client chat log, but only when called manually from the console, not from mods.
  3. When running with caves, print statements from my mod do show up in the console, but when I'm printing all the members of a table (to see what members it has for debugging) the console's 20 lines of buffer are quickly exceeded. As far as i can tell there's no way to scroll back or extend this buffer. This would be less of an issue if all output was actually appearing in my logs.

Any insights would be appreciated!

Link to comment
Share on other sites

Yup, grepping through databundles/scripts is pretty much the way to do it.

Printing on dedicated servers (which is how servers with caves work) goes to the server_log, which you can find in Documents/Klei/DoNotStarveTogether, but it takes a little hunting, and there will be one for the surface and one for the caves (they're separate servers).

Usually when debugging I keep the log files themselves open in a text editor so I can search them and scroll.

Link to comment
Share on other sites

@rezecib

I was looking in both the Caves and Master directories for the clusters I was on, but wasn't seeing my output in any of the log files in those directories.

I was surprised to not see the output in either location despite seeing it in the console.

I'll double check when I get home to make sure it wasn't just user error. 

Link to comment
Share on other sites

For the code running on the client, the player prefabs do not have an inventory component, but instead have an inventory_replica component. You can only access their inventory component directly on the server-side, so you may just need to limit your code to not run on the clients. If you want to do something on the client side, you have to go through the inventory_replica component, which is limited in its capabilities.

Unless I'm a complete idiot ^^

Link to comment
Share on other sites

@rezecib
Hmm, now that I'm looking again I'm actually seeing the print statement out put in the client_log.txt when running with caves. This seems to confirm what Ultroman is getting at. My code is running on the client when it needs to be running on the server.
 

@Ultroman
Yep, you're right, I am able to find a replica inventory.
I think we may be starting to get to the crux of my issue. The player status screen (and thus the ban button and its onClick) executes on the client, but the inventory dropping mechanic I would like to add needs to be executed by the server. I'll need to take another look at how the original onclick function achieves this and how other mods manage it. 

I'm kind of confused now regarding where my code is actually being executed. As I currently understand it server mods can alter both client and server code. Presumably this is why certain mods have a flag in the modinfo.lua to require all clients to download the server mod as well?

Sounds like I'll need to add a command the client sends to the server on the onClick action which causes the server to execute the inventory drop. 

One thing I don't understand is why I have access to both the inventory and replica inventory when running without caves.
I just tested it again:

  • When playing on a server without caves I (Player 1) have access to both the replica and real inventories of another player (Player 2) using the code executed in my mod

 

Link to comment
Share on other sites

Yes, whenever your server mod does client-code, you need to set all_clients_require_mod = true

Sometimes that's all you need in order to fix a problem. I just had that when trying to add duplicate recipes. Both the server and the clients needed to have the recipes added, for some reason, so it did all kinds of crazy things before I did so. I'm talking MAJOR crazy stuff! It would be different depending on how many recipes I added on the server, but one scenario was: I started the server, and immediately upon entering the world

  • the fullscreen overlay usually shown when being on fire was showing constantly, but I wasn't taking damage
  • there was a small ghost icon on my sanity badge
  • my hunger badge showed my hunger as completely empty, but showed 100 when hovering the mouse over it
  • my health hadge showed the huge arrow for taking damage, but I was not taking any damage
  • I could only move for a about 20 seconds, then my character wouldn't take commands anymore, but the world continued around me
  • I did not have the Critter Lab (Rock Den) but I had the build tab for it, but it didn't work. Also, it showed that even though I was standing next to an Alchemy Engine, the only thing in the Science tab was a Science Machine which had a lightblue background
  • Similarly, for some reason the Axe build button was also blue, but I couldn't craft it as I didn't have the materials

And that was just one of the, I think, 8 scenarios I would get, depending on which recipes I put in xD

1 hour ago, L. Ringmaster said:
  • When playing on a server without caves I (Player 1) have access to both the replica and real inventories of another player (Player 2) using the code executed in my mod

That sounds weird, but apparently that's how it is.

 

Anyway, following the trail of the ban-button's functionality, it looks like it ends up sending a command into the game's black box using the function UserCommands.UserRunCommandResult

Not sure what actually happens, but I'd guess it just calls Remove() on the player entity, which is probably the same function used when people just leave the server voluntarily.

Link to comment
Share on other sites

4 hours ago, Ultroman said:

Yes, whenever your server mod does client-code, you need to set all_clients_require_mod = true

I had thought about trying that, but didn't think it would be necessary since its an administrative mod, so any administrators who needed the mod could manually download it. I'll give that a try and see if it works.
@Ultroman
*EDIT: I just tried it. No luck, still only have access to replica inventories. I believe that makes sense as the ban button's is executing client side where it won't actually have full access to the player's inventory. 

 

If not.... Now that I've confirmed all_clients_require_mod isn't the solution then the following is relevant:

I had also traced the ban button back to UserCommands, but kinda stopped there. I hadn't realized (or maybe just hadn't accepted) the fact that it then enters a black box of code I won't be able to manipulate. If that's the case it sounds like I might have to find a different approach. I've also successfully used TheNet:Ban(userid) from console, so maybe I can figure out some way to implement an server side listener for a custom client side command and have the listener use TheNet:Ban.

Actually, can't I somehow cause the client to execute arbitrary code on the server? I mean... that's effectively what the client is doing when they execute remote server commands in the console, right? That's really all I want to do seeing as the user clicking the ban button must have admin permissions anyways.

 

EDIT #2: 
Just found this:

TheNet:SendRemoteExecute

Will try tomorrow!

Link to comment
Share on other sites

I haven't seen a way to do this. We'd have to ask the big boys about this.

You can try adding a function for inventory_replica to do this. I believe "classified" is the hook to the actual player. Take a look at inventory_replica.lua and e.g. its GetEquips() function

function Inventory:GetEquips()
    if self.inst.components.inventory ~= nil then
        return self.inst.components.inventory.equipslots
    else
        return self.classified ~= nil and self.classified:GetEquips() or {}
    end
end

 

Link to comment
Share on other sites

@Ultroman @rezecibI've gotten it working using TheNet:SendRemoteExecute as a work around.

Spoiler

 


local require = GLOBAL.require

local PlayerStatusScreen = require("screens/playerstatusscreen")
local OldDoInit = PlayerStatusScreen.DoInit

function MakeBannedPlayerDropEverything(playerListing)
	GLOBAL.TheNet:SendRemoteExecute("for i,player in pairs(AllPlayers) do if player.userid == \"" .. playerListing.userid .. "\" then player.components.inventory:DropEverything() end	end")
end

function PlayerStatusScreen:DoInit(ClientObjs, ...)
	OldDoInit(self, ClientObjs, ...)
	for i,playerListing in pairs(self.scroll_list.static_widgets) do
		local oldOnClick = playerListing.ban.onclick
		playerListing.ban:SetOnClick( function() 
			MakeBannedPlayerDropEverything(playerListing)
			if GetModConfigData("DISABLECONFIRMATIONMESSAGE", "testing")  then
				GLOBAL.TheNet:Ban(playerListing.userid)
			else
				oldOnClick()
			end
		end)
	end
end

 

The only problem I'm currently having is my mod config data (DISABLECONFIRMATIONMESSAGE) does not seem to work when running with caves. It always evaluates to the default even when I've configured it to be otherwise.

Link to comment
Share on other sites

Did you set all_clients_require_mod = true? The clients do not get the server settings, if this setting is false.

You should probably also just save your mod setting in a local variable, instead of reading it all the time.

local require = GLOBAL.require
local DISABLECONFIRMATIONMESSAGE = GetModConfigData("DISABLECONFIRMATIONMESSAGE") 

local PlayerStatusScreen = require("screens/playerstatusscreen")
local OldDoInit = PlayerStatusScreen.DoInit

function MakeBannedPlayerDropEverything(playerListing)
	GLOBAL.TheNet:SendRemoteExecute("for i,player in pairs(AllPlayers) do if player.userid == \"" .. playerListing.userid .. "\" then player.components.inventory:DropEverything() end	end")
end

function PlayerStatusScreen:DoInit(ClientObjs, ...)
	OldDoInit(self, ClientObjs, ...)
	for i,playerListing in pairs(self.scroll_list.static_widgets) do
		local oldOnClick = playerListing.ban.onclick
		playerListing.ban:SetOnClick( function() 
			MakeBannedPlayerDropEverything(playerListing)
			if DISABLECONFIRMATIONMESSAGE then
				GLOBAL.TheNet:Ban(playerListing.userid)
			else
				oldOnClick()
			end
		end)
	end
end

 

Also, why are you using the use_local_config parameter for the GetModConfigData() function (the second parameter, which you put "testing" in, for some reason)? That's supposed to be a bool, true/false, and setting it to true forces the mod to load the setting from the client instead of the server (I think). I removed that parameter in the code snipet above. Maybe that code snippet is enough, and you don't need all_clients_require_mod = true, but I think you do.

I keep getting confused because this topic is in the DS forum, but since we're trying to modify a ban-button, it can't be for DS xD

Link to comment
Share on other sites

@Ultroman Believe it or not that's exactly how my code originally worked. I was concerned that somehow the local variable wasn't working correctly in the OnClick function, so made the call inline. I also tested the "modname" parameter out as a shot in the dark.

You are correct, the all_clients_require_mod change was the solution.

Thanks again!

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

Please be aware that the content of this thread may be outdated and no longer applicable.

×
  • Create New...