Thread: [OSRS] Deciphering CS2 for Sliding Puzzles

Page 1 of 2 12 LastLast
Results 1 to 10 of 12
  1. #1 [OSRS] Deciphering CS2 for Sliding Puzzles 
    Registered Member

    Join Date
    May 2019
    Posts
    10
    Thanks given
    0
    Thanks received
    30
    Rep Power
    168
    This post is more a brain dump but I wanted to document how I reverse engineered the Sliding Puzzle interface in OSRS (rev 181). This tutorial will assume you are using RSMod but the concepts should be generic enough for any OSRS base.

    I have written this tutorial in the same way that I discovered and solved each task required to get the puzzle properly drawing, you will notice that step 1 is left incomplete because step 6 comes full circle to complete it.

    The Result
    https://i.gyazo.com/cf2312cbd1584ab0...e2bd9632d6.mp4


    Step 0 - Finding the interfaceId
    Spoiler for Step 0:

    Luckily this hero shared a named dump of all interface IDs in osrs https://www.rune-server.ee/runescape...rface-ids.html. An alternative may be to load it in Runelite developer tools but my Oldschool account did not have any puzzle boxes.

    The interface ID turns out to be 306.


    Step 1 - Sending pieces to the client
    Spoiler for Step 1:

    When you load the interface in game, you will be greeted with a blank grid. This is because the interface actually contains an inventory container - similar to items kept on death, looting bag, bank.
    Attached image

    The first task is to figure out how to tell the client which items to display. I stumbled upon the UpdateInvFullMessage server packet which has the following packet structure:
    Code:
      - message: gg.rsmod.game.message.impl.UpdateInvFullMessage
        type: VARIABLE_SHORT
        opcode: 70
        structure:
          - name: component_hash
            type: INT
          - name: container_key
            type: SHORT
          - name: item_count
            type: SHORT
          - name: items
            type: BYTES
    component_hash is packed as ParentId << 16 | ChildId & 65535 however it is not used in this case.

    The real challenge lies in finding the container_key. Container keys are the way for the server to send the client different groups of items and easily swap between them, rather than having to always send the array of items again (say switching from inv to looting bag - the client can just cache both in a map).

    To find the correct container key, I received a helpful tip from the r-s Discord to look at the decompiled cs2 scripts: https://github.com/RuneStar/cs2-scripts. A quick search for the terms "slide" and "puzzle" lands you on the [clientscript,trail_slidepuzzle_init] script, the important line is:
    Code:
    if_setoninvtransmit("trail_slidepuzzle_synch(false, $component0, $int5, $int6, $int3, $int4, $component1, $component2){inv_140, inv_221, inv_222}", $component0);
    With some visually scanning and assumptions, we can see inv_140, inv_221, and inv_222 may be what we're after. Let's jot down 3 container keys 140, 221, and 222.

    Finally, RSMod has a nice helper function called sendItemContainer(key, container) which we will help us send this packet.


    Step 2 - Understanding the CS2 script
    Spoiler for Step 2:

    The full trail_slidepuzzle_init script looks like so:
    Code:
    // 688
    [clientscript,trail_slidepuzzle_init](component $component0, component $component1, component $component2)
    def_int $int3 = calc(37 + ((if_getwidth($component0) - (5 * 37)) / (5 - 1)));
    def_int $int4 = calc(37 + ((if_getheight($component0) - (5 * 37)) / (5 - 1)));
    def_int $int5 = pow(5, 2);
    def_int $int6 = calc($int5 - 1);
    cc_deleteall($component0);
    def_int $int7 = 0;
    while ($int7 < $int5) {
    	cc_create($component0, ^iftype_text, $int7);
    	cc_setsize(37, 37, ^setsize_abs, ^setsize_abs);
    	cc_setposition(calc(($int7 % 5) * $int3), calc(($int7 / 5) * $int4), ^setpos_abs_left, ^setpos_abs_top);
    	cc_setopbase("<col=ff9040>Sliding piece</col>");
    	$int7 = calc($int7 + 1);
    }
    $int7 = 0;
    while ($int7 < $int6) {
    	cc_create($component0, ^iftype_model, calc($int7 + $int5));
    	cc_setsize(37, 37, ^setsize_abs, ^setsize_abs);
    	cc_setmodelangle(0, 0, 513, 0, 0, 1800);
    	$int7 = calc($int7 + 1);
    }
    ~trail_slidepuzzle_synch($component0, $int5, $int6, $int3, $int4, $component1, $component2);
    if_setoninvtransmit("trail_slidepuzzle_synch(false, $component0, $int5, $int6, $int3, $int4, $component1, $component2){inv_140, inv_221, inv_222}", $component0);
    if_setonvartransmit("trail_slidepuzzle_synch(true, $component0, $int5, $int6, $int3, $int4, $component1, $component2){var261}", $component0);
    With some refactoring, we can build the following pseudo code:
    Code:
    trail_slidepuzzle_init (component container, component border1, component border2)
        pieceWidth = 37 + ((containerWidth - (5 * 37)) / (5 - 1));
        pieceHeight = 37 + ((containerHeight - (5 * 37)) / (5 - 1));
        pieceCount = 25
        modelCount = 24 // pieces - 1
    
        loop (0 to pieceCount) {
            create new text component with absolute size 37x37 
            set text component position offset in the puzzle based on it's index
            add left-click OP with text "<col=ff9040>Sliding piece</col>"
        }
    
        loop (0 to modelCount) {
            create new model component with abs size 37x37
            rotate and scale the model so it aligns with screen
        }
    
        trail_slidepuzzle_synch(container, pieceCount, modelCount, pieceWidth, pieceHeight, component1, component2)
        
        bindObserver -> if inv_140, inv_221, or inv_222 change, call trail_slidepuzzle_synch()
        bindObserver -> if var261 changes, call trail_slidepuzzle_synch()
    border1 and border2 we will discover are the clue scroll border and the monkey madness border.
    As we can see the interface in the cache is empty and this client script actually fills in the slots for the pieces - gives each piece a click option and a model to display an item.


    Step 3 - More CS2 Deciphering & Varp
    Spoiler for Step 3:

    The slidepuzzle_init func ends with a call to trail_slidepuzzle_synch, let's take a look at it:
    Code:
    // 690
    [proc,trail_slidepuzzle_synch](component $component0, int $int1, int $int2, int $int3, int $int4, component $component5, component $component6)
    def_inv $inv7 = inv_140;
    if (%var261 = 8) {
    	$inv7 = inv_222;
    } else if (%var261 = 7) {
    	$inv7 = inv_221;
    }
    def_enum $enum8 = null;
    def_enum $enum9 = null;
    $enum8, $enum9 = enum(int, enum, enum_1864, %var261), enum(int, enum, enum_1865, %var261);
    if ((%var261 = 7) | (%var261 = 8)) {
    	if_sethide(true, $component5);
    	if_sethide(false, $component6);
    } else {
    	if_sethide(false, $component5);
    	if_sethide(true, $component6);
    }
    def_obj $obj10 = null;
    def_int $int11 = 0;
    while ($int11 < $int1) {
    	if (($enum8 ! null) & ($enum9 ! null)) {
    		$obj10 = inv_getobj($inv7, $int11);
    		~trail_slidepuzzle_setint($int11, enum(obj, int, $enum9, $obj10));
    		if (cc_find($component0, $int11) = ^true) {
    			if (%var261 = 8) {
    				cc_clearops;
    				cc_setonop(null);
    			} else {
    				cc_setop(1, "Move");
    				cc_setonop("trail_slidepuzzle_op(event_opindex, event_com, event_comid, $int1, $int2, $int3, $int4)");
    			}
    		}
    		if (($int11 < $int2) & (cc_find($component0, calc($int1 + $int11)) = ^true)) {
    			$obj10 = enum(int, namedobj, $enum8, $int11);
    			if ($obj10 ! null) {
    				cc_setmodel(oc_param($obj10, param_186));
    			} else {
    				cc_setmodel(null);
    			}
    		}
    	} else {
    		~trail_slidepuzzle_setint($int11, -1);
    		if (($int11 < $int2) & (cc_find($component0, calc($int1 + $int11)) = ^true)) {
    			cc_setmodel(null);
    		}
    	}
    	$int11 = calc($int11 + 1);
    }
    ~trail_slidepuzzle_position($component0, $int1, $int2, $int3, $int4);
    We quickly notice a reference to %var261 and with some more assumptions, we can guess that this is a varp. A varp is the way for the server to set variables on the client by sending the following packet:
    Code:
      - message: gg.rsmod.game.message.impl.VarpSmallMessage
        type: FIXED
        opcode: 63
        structure:
          - name: value
            type: BYTE
            trans: ADD
          - name: id
            type: SHORT
            trans: ADD
    In our case, the cs2 script is trying to check the varp 261 for value 7 or 8. We will eventually realize that 7 and 8 are the special cases for Monkey Madness (7 = solving the puzzle, 8 = reviewing the completed puzzle/locked).

    Simply put, the sync script checks if the varp is 7 or 8 and then changes the border of the interface by hiding and unhiding border1 and border2 respectively


    Step 4 - CS2 Enums
    Spoiler for Step 4:

    The line
    Code:
    $enum8, $enum9 = enum(int, enum, enum_1864, varp_261), enum(int, enum, enum_1865, varp_261);
    can be roughly refactored to
    Code:
        
    pieceSet = PIECE_COLLECTIONS[varp_261] // enum stored in cache - what picture is drawn - elvarg, gnome glider, zulrah
    solvedPositions = PIECE_POSITIONS[varp_261] // enum stored in cache - map of piece itemId -> correct index/position
    Basically, enums are hashmaps packed in to the cache. Using OpenOSRS cache we can dump all the enums in to json format. See enum_1864:
    Code:
    {
      "id": 1864,
      "intVals": [
        1354,
        1355,
        1356,
        1357,
        1358,
        1359,
        1872,
        1874
      ],
      "keyType": "INTEGER",
      "valType": "ENUM",
      "defaultString": "null",
      "defaultInt": -1,
      "size": 8,
      "keys": [
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8
      ]
    }
    The CS2 script is referencing keys[var261] from the enum, which returns the corresponding intVals (or stringVals) value.

    The synch script is made even more confusing by the fact that it uses an enum inside of an enum. Observe the following lines
    Code:
    ~trail_slidepuzzle_setint($int11, enum(obj, int, $enum9, $obj10));
    can become
    ~trail_slidepuzzle_setint(i, solvedPositions[piece]);
    
    and 
    
    
    $obj10 = enum(int, namedobj, $enum8, $int11);
    can become
    piece = pieceSet[i];


    Step 5 - Actually configuring the interface
    Spoiler for Step 5:

    So we know we need to init the puzzle script, which requires the parameters: container component and 2 border components. The component values must be a componentHash. Finding the correct component hashes is (as far as I know) going to take some guessing and trial and error.

    I accomplished this by inspecting the slide puzzle interface hierarchy via OpenOSRS dumper. The layers of the interface are:
    Code:
    idx 0 - id 20054016, type 0, rawWidth 0, rawHeight 0, modelId 0
    idx 1 - id 20054017, type 6, rawWidth 32, rawHeight 32, modelId 3437
    idx 2 - id 20054018, type 6, rawWidth 32, rawHeight 32, modelId 3438
    idx 3 - id 20054019, type 6, rawWidth 32, rawHeight 32, modelId 4695
    idx 4 - id 20054020, type 0, rawWidth 193, rawHeight 193, modelId 0
    idx 5 - id 20054021, type 5, rawWidth 26, rawHeight 23, modelId 0
    Type 0 is a container type, type 6 is a model widget, and type 5 is a text widget. Knowing we need a container, we will try 20054016 and 20054020 as our first parameter to the init script. The borders are possibly 20054017, 20054018, or 20054019.

    After some trial and error, I discovered that the following set of server packets:
    Code:
        
    player.setVarp(261, 7)
        player.openInterface(SLIDING_PUZZLE_INTERFACE_ID, InterfaceDestination.MAIN_SCREEN)
        player.runClientScript(688, 20054016, 20054018, 20054019)
    produced this result:
    Attached image

    Clearly 20054016 is the incorrect container because a piece is floating outside of the interface. This leaves 20054020 which results in the following:
    Attached image

    Secondly, we must discover what the 2 border component IDs should be. With 3 options I opened my Map Editor and harcoded the object picker to spit out modelIds.

    Widget 20054017 - modelId 3437 looks like:
    Attached image
    Hmm, maybe the background?

    Widget 20054018 - modelId 3438 looks like:
    Attached image
    Looks like the default puzzle border

    Widget 20054019 - modelId 4695 looks like:
    Attached image
    The fancy Monkey Madness border!

    Great, so it looks like we found our 2 border component IDs - 20054018 and 20054019.


    Step 6 - Filling with Pieces
    Spoiler for Step 6:

    We have all the pieces we need, now we just need to send them down to the client. Going to https://everythingrs.com/tools/osrs/itemlist/ and searching for "Sliding" results in 100s of pieces. We could also get the piece ids from the PIECE_COLLECTIONS enum dump if we wanted to.

    The code to fill our puzzle with piece items is very simple:
    Code:
        val puzzContainer = ItemContainer(world.definitions, capacity = 25, stackType = ContainerStackType.NO_STACK)
        for (i in 2749..2772) {
            puzzContainer.add(i)
        }
        player.sendItemContainer(key = 140, container = puzzContainer)
    
        val mmContainer = ItemContainer(world.definitions, capacity = 25, stackType = ContainerStackType.NO_STACK)
        for (i in 3904..3998 step 2) {
            mmContainer.add(i)
        }
        player.sendItemContainer(key = 221, container = mmContainer)
        player.sendItemContainer(key = 222, container = mmContainer)
    Recall from step 1 when we found that the item container keys were 140, 221, and 222.

    Note that the pieces you fill container 140 with MUST correspond to the cache enum keys at var261 or the puzzle will not be interactable.

    Finally, to tell the client to send button click packets, we must tell it which interface to bind. This can be done via:
    Code:
    player.setInterfaceEvents(interfaceId = 306, component = 4, range = 0..25, setting = 1086)
    Each piece of the puzzle is a button which when clicked sends the client packet IfButtonMessage so you can see which slot was clicked. The server must follow the same rules as the client in order to stay in sync. Puzzle solving is left as an exercise for the reader.



    I hope someone can find parts of this tutorial useful! Let me know if there are any errors or inconsistencies as I have only spent a day on this stuff.
    Reply With Quote  
     


  2. #2  
    Registered Member
    Melvin's Avatar
    Join Date
    Aug 2011
    Posts
    1,143
    Thanks given
    546
    Thanks received
    412
    Rep Power
    1486
    ah Taylor, the only bronzie thats not a brainlet


    A creative man is motivated by the desire to achieve, not by the desire to beat others.


    ― Ayn Rand
    Reply With Quote  
     

  3. #3  
    Vitality

    Raw Envy's Avatar
    Join Date
    Dec 2010
    Posts
    3,036
    Thanks given
    869
    Thanks received
    1,186
    Rep Power
    3054
    Haven't seen a good tutorial in a long time, haha nice one
    Reply With Quote  
     

  4. #4  
    Donator

    Kid Buu's Avatar
    Join Date
    Aug 2017
    Posts
    431
    Thanks given
    177
    Thanks received
    226
    Rep Power
    399
    This is super informative & useful. Thanks for this.
    Reply With Quote  
     

  5. #5  
    Respected Member


    Kris's Avatar
    Join Date
    Jun 2016
    Age
    25
    Posts
    3,638
    Thanks given
    804
    Thanks received
    2,626
    Rep Power
    5000
    Decent, happy to see more people actually logically approaching these problems.

    A few tips I can give on how to make this process easier would be regarding this:
    Quote Originally Posted by shocker View Post
    component_hash is packed as ParentId << 16 | ChildId & 65535 however it is not used in this case.

    The real challenge lies in finding the container_key. Container keys are the way for the server to send the client different groups of items and easily swap between them, rather than having to always send the array of items again (say switching from inv to looting bag - the client can just cache both in a map).

    To find the correct container key, I received a helpful tip from the r-s Discord to look at the decompiled cs2 scripts: https://github.com/RuneStar/cs2-scripts. A quick search for the terms "slide" and "puzzle" lands you on the [clientscript,trail_slidepuzzle_init] script, the important line is:
    Code:
    if_setoninvtransmit("trail_slidepuzzle_synch(false, $component0, $int5, $int6, $int3, $int4, $component1, $component2){inv_140, inv_221, inv_222}", $component0);
    With some visually scanning and assumptions, we can see inv_140, inv_221, and inv_222 may be what we're after. Let's jot down 3 container keys 140, 221, and 222.
    You just did a string search on the CS2s to locate it, however there is a more logical approach to this. Just about all interfaces have some sort of an "on-load script" that is executed client-side when you open an interface. These scripts are what set all these "if_setonxtransmit" hooks and what effectively makes your client automatically listen to stuff like varps and container changes.
    You can find out what scripts are linked to specific interface components through the definitions, there's a load of different Object arrays in there. Each of those is a static way to link specific CS2s with that component/interface.

    Attached image
    You can find the same obfuscated code within your client, however this is the order in which these listeners are decoded within these component definitions.

    This lets you extract the scripts that are linked to interfaces, I personally do it in a json format like this:
    Code:
      {
        "interfaceId": 306,
        "components": [
          {
            "componentId": 0,
            "format": "New",
            "type": "Layer",
            "scripts": {
              "Load": {
                "scriptId": 688,
                "parameters": [
                  "306 << 16 | 4; 20054020",
                  "306 << 16 | 2; 20054018",
                  "306 << 16 | 3; 20054019"
                ]
              }
            }
          },
          {
            "componentId": 1,
            "format": "New",
            "type": "Model"
          },
          {
            "componentId": 2,
            "format": "New",
            "type": "Model"
          },
          {
            "componentId": 3,
            "format": "New",
            "type": "Model"
          },
          {
            "componentId": 4,
            "format": "New",
            "type": "Layer"
          },
          {
            "componentId": 5,
            "format": "New",
            "type": "Graphic",
            "actions": [
              "Close"
            ],
            "scripts": {
              "Op": {
                "scriptId": 29
              },
              "Mouse over": {
                "scriptId": 44,
                "parameters": [
                  "Param -> Bitpacked source component",
                  540
                ]
              },
              "Mouse leave": {
                "scriptId": 44,
                "parameters": [
                  "Param -> Bitpacked source component",
                  539
                ]
              }
            }
          }
        ]
      },
    You can see that the on_load script in my extraction here has an id of 688 and that matches the id of the trail_slidepuzzle_init script in the repository.
    Attached image
    Reply With Quote  
     

  6. Thankful users:


  7. #6  
    Developer


    Join Date
    Aug 2012
    Age
    25
    Posts
    2,492
    Thanks given
    180
    Thanks received
    1,728
    Rep Power
    2472
    Quote Originally Posted by Kris View Post
    Decent, happy to see more people actually logically approaching these problems.

    A few tips I can give on how to make this process easier would be regarding this:

    You just did a string search on the CS2s to locate it, however there is a more logical approach to this. Just about all interfaces have some sort of an "on-load script" that is executed client-side when you open an interface. These scripts are what set all these "if_setonxtransmit" hooks and what effectively makes your client automatically listen to stuff like varps and container changes.
    You can find out what scripts are linked to specific interface components through the definitions, there's a load of different Object arrays in there. Each of those is a static way to link specific CS2s with that component/interface.

    You can find the same obfuscated code within your client, however this is the order in which these listeners are decoded within these component definitions.

    This lets you extract the scripts that are linked to interfaces, I personally do it in a json format like this:

    You can see that the on_load script in my extraction here has an id of 688 and that matches the id of the trail_slidepuzzle_init script in the repository.
    kris is very right on this one, and if you feel uncomfortable going through obfuscated code / figuring out how an interface works, i strongly recommend using Admiraal Slee's interface editor. the editor allows you to load up an interface and instantly see how and which cs2 scripts are linked to a certain interface
    Reply With Quote  
     

  8. #7  
    Registered Member

    Join Date
    May 2019
    Posts
    10
    Thanks given
    0
    Thanks received
    30
    Rep Power
    168
    Thanks Kris for the information that is really helpful to learn! Exactly the type of feedback I was looking for.
    Reply With Quote  
     

  9. #8  
    Retired From RSPS

    iGarrett's Avatar
    Join Date
    Dec 2013
    Posts
    458
    Thanks given
    142
    Thanks received
    109
    Rep Power
    166
    This is what i like, a good quality tutorial. Quality non-trash replies.
    For a second i think rune-server has some hope left for the future.

    Thank for this.
    I was actually wondering the other day on how these worked and now i know!
    Quote Originally Posted by i am here View Post
    I have never messed with Eclipse. Is it a whole new revision or type of code?
    Quote Originally Posted by bibl View Post
    hahaha, good one m9. "deob" is short for "deobfuscated" which is not the same as decompiled.
    I'm GEEGIN' OUT
    Reply With Quote  
     

  10. #9  
    Banned


    Join Date
    Dec 2007
    Age
    35
    Posts
    12,573
    Thanks given
    198
    Thanks received
    7,086
    Rep Power
    0
    Good work bro, well explained, thanks.
    Reply With Quote  
     

  11. #10  
    Registered Member

    Join Date
    Feb 2010
    Posts
    3,252
    Thanks given
    1,145
    Thanks received
    902
    Rep Power
    2017
    Quote Originally Posted by Melvin View Post
    ah Taylor, the only bronzie thats not a brainlet
    Yeah lots of bronzes here are probably banned members tbh lol
    Reply With Quote  
     

Page 1 of 2 12 LastLast

Thread Information
Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)


User Tag List

Similar Threads

  1. Replies: 1
    Last Post: 02-01-2020, 09:39 PM
  2. [OSRS] Vidor - Full OSRS Experience | Looking for developers!
    By Hyperr in forum Projects
    Replies: 13
    Last Post: 04-23-2018, 03:36 PM
  3. Replies: 2
    Last Post: 08-02-2014, 10:16 AM
  4. Osrs items & cash for sale!!!
    By MaxXi in forum Gold & Items
    Replies: 2
    Last Post: 07-19-2013, 03:13 PM
Posting Permissions
  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •