inventory-restore.sk

Created by Sovde

Just so you know, we don't know the file format for every file. If it's just a bunch of random characters, it's probably a .zip or .jar.

# Inventory Restoration
# by Sovde (@Sovde#0001)

# Requires SkBee 2.8.2+ (should work on 2.5.0+, but untested) and skript-gui 1.3+

# Stores backups of inventories when players die using NBT files.
# The files can be found in the directory folder (see options), labeled with the player's UUID.
# By default, backups are stored for 30 days, but you can change that in the options

# Backups can be viewed, edited, restored, and deleted using a GUI.
# Edits will only be saved when the player clicks the "Save edits" button in the GUI.

# When restoring, items will be placed back in the same slots they were in when the player died, if possible.
# If the player has no space in their inventory, items will be dropped on the ground.

options:
    # How long backups are stored for
    # Set to -1 to store backups forever
    age-limit: 30 days

    # Where to store backups
    directory: plugins/inventory-restore

    # Colours
    accent: <##aabbff>
    main: <##9999a0>
    warning: <##f5a147>
    error: <##cc3434>
    success: <##22aa6a>

    # error/success messages:
    # by default looks like "[!] Message here."
    prefix: [!]
    # when backups are restored to the player ({_target})
    restored-me-sucess: Your inventory has been restored!
    restored-sucess: %{_target}%'s inventory has been restored!
    # when backup edits are saved
    saved-sucess: Your edits have been saved!
    # when backups are deleted
    deleted-sucess: Backup deleted!
    # when backups can't be found
    non-existent-error: That backup does not exist!
    # when the player ({_target}) is offline
    offline-error: %{_target}% is offline! 



command /invrestore <offlineplayer>:
    permission: invrestore.restore
    trigger:
        openInvRestoreGUI(player, arg-1)


# storing death data
on death of player:
    # setup nbt
    set {_entry} to blank nbt compound 
    set {_now} to unix timestamp of now

    # inventory data
    loop items in victim's inventory:
        set {_nbt} to full nbt compound copy of loop-value
        set tag "Slot" of {_nbt} to index of loop-value
        set {_items::%index of loop-value%} to {_nbt}
    
    set compound list tag "Inventory" of {_entry} to {_items::*}
    
    # metadata
    set tag "Message" of {_entry} to death message
    set long tag "Timestamp" of {_entry} to {_now} * 1000

    set {_location} to victim's location
    set float list tag "Position" of {_entry} to (x coordinate of {_location}), (y coordinate of {_location}), (z coordinate of {_location})
    set tag "World" of {_entry} to "%world of {_location}%"

    # save to file
    set {_file} to nbt from file "{@directory}/%victim's uuid%-deaths.nbt"
    set {_entries::*} to compound list tag "Deaths" of {_file}
    add {_entry} to {_entries::*}

    # clean up old backups
    set {_limit} to {@age-limit}
    if {_limit} is not -1:
        loop {_entries::*}:
            set {_timestamp} to long tag "Timestamp" of loop-value
            set {_date} to unix date of ({_timestamp} / 1000)
            if time since {_date} >= {_limit}:
                remove loop-value from {_entries::*}

    set compound list tag "Deaths" of {_file} to {_entries::*}
    save nbt file of {_file}



# main gui
function openInvRestoreGUI(viewer: player, target: offlineplayer, page: number = 1):
    # get entries from file
    set {_file} to nbt of file "{@directory}/%{_target}'s uuid%-deaths.nbt"
    set {_entries::*} to compound list tag "Deaths" of {_file}

    # clean up old backups
    set {_limit} to {@age-limit}
    if {_limit} is not -1:
        loop {_entries::*}:
            set {_timestamp} to long tag "Timestamp" of loop-value
            set {_date} to unix date of ({_timestamp} / 1000)
            if time since {_date} >= {_limit}:
                remove loop-value from {_entries::*}
    set compound list tag "Deaths" of {_file} to {_entries::*}
    save nbt file of {_file}

    # order entries by timestamp, newest to oldest
    loop {_entries::*}:
        set {_timestamp} to long tag "Timestamp" of loop-value
        set {_ordered-entries::%{_timestamp}%} to loop-value
    set {_ordered-entries::*} to reversed {_ordered-entries::*}

    # set up gui
    create a gui with virtual chest inventory with 3 rows named "{@main}Inventory Backups":
        format gui slot 4 with {_target}'s skull named "{@accent}%{_target}%{@main}'s Inventory Backups"
        set {_slot} to 11
        # manage pagination
        set {_counter} to 0
        set {_start} to 1 + (5 * ({_page} - 1))
        set {_end} to 5 + (5 * ({_page} - 1))
        loop {_ordered-entries::*}:
            add 1 to {_counter}
            # skip entries outside of page range
            if {_counter} is not between {_start} and {_end}:
                continue

            set {_item} to a skeleton skull named "Death"
            clear {_lore::*}

            # add meta-data to item lore
            add "&7&o""%tag "Message" of loop-value%""" to {_lore::*}
            set {_timestamp} to long tag "Timestamp" of loop-value
            add "{@accent}Timestamp: &f%unix date of {_timestamp} / 1000%" to {_lore::*}
            set {_pos::*} to float list tag "Position" of loop-value
            set {_pos::*} to "%{_pos::1}%", "%{_pos::2}%", "%{_pos::3}%"
            add "{@accent}Location: &f%join {_pos::*} with ", "% {@main}in &f%tag "World" of loop-value%" to {_lore::*}

            add "" to {_lore::*}

            # add instructions to lore
            add "{@main}Left-click &fto {@accent}view &fthe backup" to {_lore::*}
            add "{@main}Right-click &fto {@accent}restore &fthe backup" to {_lore::*}
            add "{@main}Shift-left-click &fto {@accent}teleport &fthe to death location" to {_lore::*}
            add "{@main}Shift-right-click &fto {@accent}delete &fthe backup" to {_lore::*}

            # check if backup has already been restored to player
            if loop-value has tag "restored":
                add "%nl%{@warning}This backup has been previously restored!" to {_lore::*}

            # add nbt tag for click events
            set long tag "Timestamp" of (nbt of {_item}) to {_timestamp}

            set lore of {_item} to {_lore::*}
            set {_entry} to loop-value

            # add item to gui, set click events
            format gui slot {_slot} with {_item}:
                # view backup
                if gui click type is left mouse button:
                    viewInvRestoreBackup({_viewer}, {_target}, {_timestamp})

                # restore backup
                else if gui click type is right mouse button:
                    restoreInvRestoreBackup({_viewer}, {_target}, {_entry}, {_file})
                    openInvRestoreGUI({_viewer}, {_target})

                # teleport to death location
                else if gui click type is left mouse button with shift:
                    set {_pos::*} to float list tag "Position" of {_entry}
                    set {_world} to tag "World" of {_entry}
                    close {_viewer}'s inventory
                    wait 1 tick
                    teleport {_viewer} to location({_pos::1}, {_pos::2}, {_pos::3}, {_world})

                # delete backup
                else if gui click type is right mouse button with shift:
                    deleteInvRestoreBackup({_target}, {_timestamp}, {_file})
                    openInvRestoreGUI({_viewer}, {_target})

            # increment slot
            add 1 to {_slot}
            if {_slot} is 16:
                exit loop
        
        if {_page} is not 1:
            format gui slot 21 to a book named "{@main}Previous Page":
                openInvRestoreGUI({_viewer}, {_target}, ({_page} - 1))
        if size of {_ordered-entries::*} is more than {_end}:
            format gui slot 23 to a book named "{@main}Next Page":
                openInvRestoreGUI({_viewer}, {_target}, ({_page} + 1))

    open last gui to {_viewer}


# viewing the backups
function viewInvRestoreBackup(viewer: player, target: offlineplayer, id: number):
    # get entry from file
    set {_file} to nbt of file "{@directory}/%{_target}'s uuid%-deaths.nbt"
    set {_entries::*} to compound list tag "Deaths" of {_file}
    loop {_entries::*}:
        if long tag "Timestamp" of loop-value is {_id}:
            set {_entry} to loop-value
            exit loop

    # ensure entry exists
    if {_entry} is not set:
        send "{@error}{@prefix}{@main} {@non-existent-error}" to {_viewer}
        stop

    # set up gui
    create gui with virtual chest inventory with 6 rows named formatted "&8Backup for %{_target}%":
        set {_items::*} to compound list tag "Inventory" of {_entry}

        # ensure all inv slots can be modified
        loop integers from 0 to 40:
            format gui slot loop-value with removable air

        # add items to gui
        loop {_items::*}:
            set {_item} to item from nbt loop-value
            set {_slot} to tag "Slot" of loop-value
            format gui slot {_slot} with removable {_item}

        # controls:

        # return to overview
        format gui slot 46 to a red stained glass pane named "{@main}Return to {@accent}Overview":
            openInvRestoreGUI({_viewer}, {_target})

        # save edits
        format gui slot 48 to a yellow stained glass pane named "{@accent}Save edits {@main}to this backup":
            saveInvRestoreBackup({_viewer}, {_id}, {_file})
            viewInvRestoreBackup({_viewer}, {_target}, {_id})

        # restore backup
        format gui slot 50 to a green stained glass pane named "{@accent}Restore {@main}this backup to {@accent}%{_target}%" with lore "{@accent}Shift-click {@main}to {@warning}clear the recipient's inventory{@main} first.":
            if gui click type is left mouse button with shift or right mouse button with shift:
                clear {_target}'s inventory
            restoreInvRestoreBackup({_viewer}, {_target}, {_entry}, {_file})

        # teleport to death location    
        format gui slot 52 to an ender pearl named "{@accent}Teleport {@main}to the death loation":
            set {_pos::*} to float list tag "Position" of {_entry}
            set {_world} to tag "World" of {_entry}
            close {_viewer}'s inventory
            wait 1 tick
            teleport {_viewer} to location({_pos::1}, {_pos::2}, {_pos::3}, {_world})

        # delete backup
        format gui slot 53 to a barrier named "{@error}Delete {@main}this backup":
            deleteInvRestoreBackup({_target}, {_id}, {_file})
            openInvRestoreGUI({_viewer}, {_target})     
    
    open last gui to {_viewer}


# restoring the backups
function restoreInvRestoreBackup(viewer: player, target: offlineplayer, entry: nbtcompound, file: nbtcompound):
    # ensure player is online to receive items
    if {_target} is offline:
        send "{@error}{@prefix}{@main} {@offline-error}" to {_viewer}
        stop

    # get inventory data
    set {_items::*} to compound list tag "Inventory" of {_entry}

    # loop through items
    loop {_items::*}:
        set {_slot} to tag "Slot" of loop-value
        set {_item} to item from nbt loop-value

        # add to original slot if possible, otherwise give to player if they have space, otherwise drop
        if slot {_slot} of {_target}'s inventory is air:
            set slot {_slot} of {_target}'s inventory to {_item}
        else if {_target} has space for {_item}:
            give {_item} to {_target}
        else:
            drop {_item} at {_target}

    # send success message
    send "{@success}{@prefix}{@main} {@restored-me-sucess}"  to {_target}
    send "{@success}{@prefix}{@main} {@restored-sucess}"  to {_viewer}

    # mark backup as restored
    set {_id} to long tag "Timestamp" of {_entry}
    set {_entries::*} to compound list tag "Deaths" of {_file}
    loop {_entries::*}:
        if long tag "Timestamp" of loop-value is {_id}:
            set {_entry} to loop-value
            exit loop
    set tag "restored" of {_entry} to true
    set compound list tag "Deaths" of {_file} to {_entries::*}
    # save file
    save nbt file of {_file}


# Saving edits to backups
function saveInvRestoreBackup(viewer: player, id: number, file: nbtcompound):
    # get entry from file
    set {_entries::*} to compound list tag "Deaths" of {_file}
    loop {_entries::*}:
        if long tag "Timestamp" of loop-value is {_id}:
            set {_entry} to loop-value
            exit loop
    
    # ensure entry exists
    if {_entry} is not set:
        send "{@error}{@prefix}{@main} {@non-existent-error}" to {_viewer}
        stop
    
    # get inventory data
    loop integers from 0 to 40:
        set {_item} to slot loop-value of {_viewer}'s current inventory
        if {_item} is air:
            continue
        set {_nbt} to full nbt compound copy of {_item}
        set tag "Slot" of {_nbt} to loop-value
        set {_items::%loop-value%} to {_nbt}
    
    # save inventory data to entry
    set compound list tag "Inventory" of {_entry} to {_items::*}
    set compound list tag "Deaths" of {_file} to {_entries::*}

    # save file
    save nbt file of {_file}

    send "{@success}{@prefix}{@main} {@saved-sucess}" to {_viewer}


# deleting backups
function deleteInvRestoreBackup(viewer: player, id: number, file: nbtcompound):
    # get entry from file
    set {_entries::*} to compound list tag "Deaths" of {_file}
    loop {_entries::*}:
        if long tag "Timestamp" of loop-value is {_id}:
            set {_entry} to loop-value
            exit loop

    # ensure entry exists
    if {_entry} is not set:
        send "{@error}{@prefix}{@main} {@non-existent-error}" to {_viewer}
        stop

    # remove entry from file
    remove {_entry} from {_entries::*}
    set compound list tag "Deaths" of {_file} to {_entries::*}
    save nbt file of {_file}

    send "{@success}{@prefix}{@main} {@deleted-sucess}" to {_viewer}