Last active
September 19, 2024 19:44
-
-
Save umnikos/2329182b06714a7815cfead9bc45883e to your computer and use it in GitHub Desktop.
SC3 `\diamond` sell shop's code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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