Skip to content

Instantly share code, notes, and snippets.

@umnikos
Last active September 19, 2024 19:44
Show Gist options
  • Save umnikos/2329182b06714a7815cfead9bc45883e to your computer and use it in GitHub Desktop.
Save umnikos/2329182b06714a7815cfead9bc45883e to your computer and use it in GitHub Desktop.
SC3 `\diamond` sell shop's code
if not chatbox.hasCapability("command") or not chatbox.hasCapability("tell") then
error("Chatbox does not have the required permissions. Did you register the license?")
end
local helper = require "helper"
-- SETUP:
-- have a turtle with an entity sensor on its right (to detect player presence) and wireless modem on its left (for shopsync)
-- have slots 2-16 full of dummy items (gold swords, for example), only leaving slot 1 empty
-- and finally, place a chest below the turtle (that's where received diamonds will be stored)
-- constants
local diamonds_per_krist = 12
local bot_name = "Diamond buyer"
local WALLET_PKEY = "REDACTED"
local help_message = [[
This is an automated way to sell diamonds for a fixed price! Located next to umni's shop. Here's how to use it:
1. Type "\diamond begin" to begin a transaction
2. Toss diamonds on top of the turtle.
3. Type "\diamond end" to receive your krist and end the session!
The current price is 0.08333 per diamond.
]]
-- global state
local locked = false
local user_in_session = nil
local krist_pending = 0
local last_activity = nil
function save()
local data = {
user_in_session = user_in_session,
krist_pending = krist_pending,
last_activity = last_activity,
}
local str = helper.serialize(data)
helper.delete_file("new_state.valid")
helper.write_file("new_state.state", str)
helper.write_file("new_state.valid", "true")
helper.delete_file("current_state.valid")
helper.write_file("current_state.state", str)
helper.write_file("current_state.valid", "true")
helper.delete_file("new_state.valid")
helper.delete_file("new_state.state")
end
function load()
local str = nil
if not str then
if fs.exists("current_state.state") and fs.exists("current_state.valid") then
str = helper.read_file("current_state.state")
end
end
if not str then
if fs.exists("new_state.state") and fs.exists("new_state.valid") then
str = helper.read_file("new_state.state")
end
end
if str then
local data = helper.deserialize(str)
user_in_session = data.user_in_session
krist_pending = data.krist_pending
last_activity = data.last_activity
end
end
function lock()
while locked do sleep(0) end
locked = true
end
function unlock()
if not locked then
print("WARNING: UNLOCKING WHEN NOT LOCKED?!?")
end
save()
locked = false
end
function log(s)
local log_file = fs.open("payouts.log","a")
log_file.writeLine(timestamp().." - "..s)
log_file.flush()
log_file.close()
end
local second = 1000
local minute = 60*second
function timestamp()
return os.epoch("utc")
end
function activity()
last_activity = timestamp()
end
function begin_session(user)
if user_in_session == user then
chatbox.tell(user, "You are already in a session", bot_name)
return
end
if user_in_session ~= nil then
chatbox.tell(user, "Another user is currently in a session, please wait for them to finish", bot_name)
return
end
local m = peripheral.wrap("right")
local entities = m.sense()
local player_found = false
for i,v in ipairs(entities) do
if v.key == "minecraft:player" and v.name == user then
player_found = true
break
end
end
if not player_found then
chatbox.tell(user, "Please come closer to the turtle to start a session, it's at x=80, z=-50 in catmall", bot_name)
return
end
activity()
chatbox.tell(user, "Beginning session as "..user, bot_name)
user_in_session = user
krist_pending = 0
end
function end_session(user)
if user_in_session ~= user then
chatbox.tell(user, "You're not in a session", bot_name)
return
end
activity()
chatbox.tell(user, "Ending session as "..user, bot_name)
turtle.drop()
user_in_session = nil
pay_krist_out(user)
end
function pay_krist_out(user)
local amount = math.floor(krist_pending)
krist_pending = 0
save()
if amount > 0 then
log("paying "..amount.." to "..user)
http.post("https://krist.dev/transactions/", textutils.serializeJSON {
privatekey = WALLET_PKEY,
to = user.."@switchcraft.kst",
amount = math.floor(amount),
}, { ["Content-Type"] = "application/json" });
end
end
function main()
--[[
for i=1,16 do
turtle.select(i)
if turtle.getItemCount() > 0 then
turtle.drop()
end
end
]]
turtle.select(1)
turtle.drop()
load()
while true do
local event, user, command, args = os.pullEvent("command")
if command == "diamond" then
lock()
if #args == 0 then
chatbox.tell(user, help_message, bot_name)
elseif args[1] == "begin" or args[1] == "start" then
begin_session(user)
elseif args[1] == "end" or args[1] == "exit" then
end_session(user)
else
chatbox.tell(user, "Unknown subcommand!", bot_name)
end
unlock()
end
end
end
function echest_is_full()
local chest = peripheral.wrap("bottom")
return (chest.getItemDetail(27) ~= nil)
end
function suck_books()
local leftover_diamonds = 0
while true do
if user_in_session ~= nil then
turtle.suckUp(64-leftover_diamonds)
local item = turtle.getItemDetail()
if not item or (item.name == "minecraft:diamond" and item.count == leftover_diamonds) then
turtle.suck(64-leftover_diamonds)
item = turtle.getItemDetail()
end
-- there were two yields here so user may no longer be in session
lock()
if user_in_session == nil then
turtle.drop()
elseif item and not (item.name == "minecraft:diamond" and item.count == leftover_diamonds) then
activity()
--print(item.nbt)
if echest_is_full() then
turtle.drop()
chatbox.tell(user_in_session, "Error occurred: Storage is full", bot_name)
elseif item.name == "minecraft:diamond" then
--turtle.drop(item.count % 10)
leftover_diamonds = item.count % diamonds_per_krist
local c = item.count - leftover_diamonds
turtle.dropDown(c)
krist_pending = krist_pending + c/diamonds_per_krist
local message = "Your balance is now: "..krist_pending
if leftover_diamonds > 0 then
message = message .. "; Pending diamonds: " ..(item.count % diamonds_per_krist)
end
chatbox.tell(user_in_session, message , bot_name)
else
turtle.drop()
end
end
unlock()
else
leftover_diamonds = 0
coroutine.yield()
end
end
end
function watchdog()
while true do
sleep(5)
lock()
if user_in_session ~= nil then
local inactivity = timestamp() - last_activity
print(inactivity)
if inactivity > 1.5*minute then
end_session(user_in_session)
end
end
unlock()
end
end
parallel.waitForAll(main, suck_books,watchdog)
local open
open = io.open
local DEBUG = false
local args = {
...
}
local read_file
read_file = function(name)
local file = open(name, "r")
if file then
local contents = file:read("*a")
file:close()
return contents
else
return nil
end
end
local write_file
write_file = function(name, contents)
local file = open(name, "w")
file:write(contents)
return file:close()
end
local delete_file
delete_file = function(name)
if fs then
return fs.delete(name)
else
return os.remove(name)
end
end
local rename_file
rename_file = function(old, new)
if fs then
if fs.exists(new) then
fs.delete(new)
end
return fs.move(old, new)
else
return os.rename(old, new)
end
end
local serialize
serialize = function(o)
local t = type(o)
if "string" == t then
local s = string.format("%q", o)
return s
end
if "number" == t then
if o == 1 / 0 then
return "(1/0)"
end
return o
end
if "table" == t then
local s = "{ "
for k, v in pairs(o) do
s = s .. "[" .. (serialize(k)) .. "] = " .. (serialize(v)) .. ", "
end
return s .. " }"
end
if "function" == t then
local str = string.dump(o)
return "loadstring(" .. serialize(str) .. ")"
end
if "boolean" == t then
return tostring(o)
end
if "nil" == t then
return "nil"
end
return error("DIDN'T THINK OF TYPE " .. (t) .. " FOR SERIALIZING")
end
local deserialize
deserialize = function(str)
local s = "return " .. str
local f = (loadstring(s))()
return f
end
return {
read_file = read_file,
write_file = write_file,
delete_file = delete_file,
rename_file = rename_file,
serialize = serialize,
deserialize = deserialize
}
--[[
ShopSync is a standard for shops and "sellshops" to broadcast data in order to improve consumer price discovery and user experience.
Shops, optionally, can abstain from broadcasting information if they are completely out-of-stock.
Note: Broadcasting any false or incorrect data is against Rule 1.5. Shops should not broadcast data if they are not connected to a currency network or are inoperable for any other reason. The intent of ShopSync was not for shops to automatically adjust their own prices based on other shops' prices, considering the current lack of any technical protections against falsified data.
This standard is presented as an example table, with comments explaining the fields. Everything that is not specifically "optional" or saying "can be set to nil" is required. Note that "set to nil" can also mean "not set".
Shops which support this standard and actively broadcast their information can optionally display "ShopSync supported", "ShopSync-compatible", etc. on monitors
- Shops should broadcast a Lua table like this on channel 9773 in the situations listed below.
- The modem return channel should be the computer ID of the shop turtle/computer modulo 65536. This is kept for backwards compatibility purposes only, the info.computerID should be the only source of computer ID used when provided.
- Any timespans in terms of seconds should be governed by os.clock() and os.startTimer()
- Shops may broadcast:
- 15 to 30 seconds after the shop turtle/computer starts up
- After the shop inventory has been updated, such as in the event of a finished transaction or restock
- When the items on sale are changed, such as price or availability
- Legacy code built on older versions of this specification may broadcast every 30 seconds instead of the situations outlined above.
The ShopSync standard is currently located at https://github.com/slimit75/ShopSync
Version: v1.2-staging, 2023-09-15
]]--
price = 0.08333
wallet_address = "kulhfutw3j"
t = {
type = "ShopSync", -- Keep this the same
version = 1, -- Required integer representing the specification version in use. Use a value of `1` for ShopSync version 1.2, a value of `nil` implies version 1.1 or prior.
info = { -- Contains general info about the shop
name = "\diamond", -- Name of shop. This is required.
description = "Buys diamonds at a fixed price", -- Optional. Brief description of shop. Try not to include anything already provided in other information fields. Can be generic (e.g. "shop selling items")
owner = "umnikos", -- Optional. Should be Minecraft username or other username that can help users easily identify shop owner
computerID = 4252, -- Integer representing the ID of the computer or turtle running the shop. If multiple turtles or computers are involved, choose whichever one is calling modem.transmit() for ShopSync. Data receivers can differentiate between unique shops using the computerID and multiShop fields. If the computerID field is not set, then data receivers should check the reply channel and use that as the computer ID.
multiShop = nil, -- If a single computer/turtle is operating multiple shops, it should assign permanent unique integer IDs to each shop. This is so that shops can be differentiated if multiple shops run on the same computer ID. This can also apply if a single computer/turtle is running both a shop and a reverse shop. Shops for which this does not apply should set this to nil.
location = { -- Optional
coordinates = { 79, 75, -53 }, -- Optional table of integers in the format {x, y, z}. Should be location near shop (where items dispense, or place where monitor is visible from). Can also be automatically determined via modem GPS, if the location is not provided in the shop configuration.
description = "Next to umnikos' shop", -- Optional. Description of location
dimension = "overworld" -- "overworld", "nether", or "end". Optional, but include this if you are including a location.
},
},
items = { -- List of items/offers the shop contains. Shops can contain multiple listings for the same item with different prices and stocks, where the item stocks should be separate (e.g. selling 100 diamonds for 10 kst and 200 diamonds for 15 kst). Shops can broadcast out-of-stock listings (where the stock = 0); ideally, they should do so based on whether the listings display on the shop monitor.
{ -- This shows an example entry for a reverse shop ("sellshop"). Shops which give items to the user should see the first example entry.
shopBuysItem = true, -- ALL DATA READERS MUST CHECK THIS FLAG! "Reverse shop listings" are for shops which accept items from a player and give Krist in exchange. These are also called "sellshops" / "pawnshops". Reverse shop listings should set this to true. Shop listings for which this does not apply can set this to false or nil.
prices = { -- Table of tables describing price(s) (in different currencies) that the reverse shop will pay for an item
{
value = 0.08333,
currency = "KST"
}
},
item = { -- Table describing item: see above
name = "minecraft:diamond",
nbt = nil,
displayName = "Diamond",
description = nil
},
stock = 12*textutils.unserializeJSON(http.get("https://krist.dev/addresses/"..wallet_address).readAll()).address.balance, -- Integer representing the current limit on amount of this item the reverse shop is willing to accept. If there is no specific item limit, shops should get the current balance, divide by the price, and round down (also see the noLimit option)
noLimit = true -- If the reverse shop listing has no limit, set this to true. In this case, a shop is willing to accept more items than it can actually pay out for. If not applicable, set to false/nil. This would usually be false/nil when dynamicPrice is true.
},
}
}
sleep(5)
while true do
m = peripheral.find("modem")
m.transmit(9773, 4252, t)
sleep(60)
end
sleep(2)
function diamond()
shell.run("diamond")
end
function shopsync()
shell.run("shopsync")
end
parallel.waitForAll(diamond,shopsync)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment