From 388525ebcb9d2c61b000b39c93d7a499bd331e72 Mon Sep 17 00:00:00 2001 From: Signal Date: Mon, 6 Oct 2025 14:06:55 -0400 Subject: [PATCH] Initial commit --- LICENSE.txt | 121 ++ init.lua | 1283 +++++++++++++++++++++ mod.conf | 2 + settingtypes.txt | 2 + textures/libskinupload_icon.png | Bin 0 -> 172 bytes textures/libskinupload_locked.png | Bin 0 -> 329 bytes textures/libskinupload_search.png | Bin 0 -> 166 bytes textures/libskinupload_tag_bg.png | Bin 0 -> 155 bytes textures/libskinupload_tag_bg_hovered.png | Bin 0 -> 155 bytes uskins_import.php | 65 ++ 10 files changed, 1473 insertions(+) create mode 100644 LICENSE.txt create mode 100644 init.lua create mode 100644 mod.conf create mode 100644 settingtypes.txt create mode 100644 textures/libskinupload_icon.png create mode 100644 textures/libskinupload_locked.png create mode 100644 textures/libskinupload_search.png create mode 100644 textures/libskinupload_tag_bg.png create mode 100644 textures/libskinupload_tag_bg_hovered.png create mode 100644 uskins_import.php diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1625c17 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..a757f6b --- /dev/null +++ b/init.lua @@ -0,0 +1,1283 @@ +libskinupload = { + notifications = {}, + reviewers = {}, + enabled = {} +} + +local mcl = not not minetest.get_modpath("mcl_skins") +local it = not not minetest.get_modpath("itbase") + +if mcl then + --Override mcl_skins to degrade gracefully instead of giving an unknown item + local _get_node_id_by_player = mcl_skins.get_node_id_by_player + function mcl_skins.get_node_id_by_player(p) + local out = _get_node_id_by_player(p) + if not minetest.registered_items["mcl_meshhand:"..out] then + return "mcl_skins_character_1_female_surv" + else + return out + end + end +end + +-- Shim for 3d_armor +if minetest.get_modpath("3d_armor") then + local _get_player_skin = armor.get_player_skin + function armor.get_player_skin(self, name) + local id = libskinupload.get_skin_id(name) + if id == "" then + return _get_player_skin(self, name) + end + return "libskinupload_uploaded_skin_"..id..".png" + end +end + +-- Shim for u_skins +if minetest.get_modpath("u_skins") then + local gt = u_skins.get_type + u_skins.get_type = function(tx) + local out = gt(tx) + if out then return out else return u_skins.type.MODEL end + end +end + +-- Shim for Nodecore +if minetest.global_exists("nodecore") then + local _player_skin = nodecore.player_skin + function nodecore.player_skin(player, options, ...) + local pname = options and options.playername or player and player:get_player_name() + local skin = pname and libskinupload.get_skin(pname) + return skin or _player_skin(player, options, ...) + end +end + + +-- Polyfill for older versions +if not minetest.hypertext_escape then + local hypertext_escapes = { + ["\\"] = "\\\\", + ["<"] = "\\<", + [">"] = "\\>", + } + function core.hypertext_escape(text) + return text and text:gsub("[\\<>]", hypertext_escapes) + end + minetest.hypertext_escape = core.hypertext_escape +end + +local function player_model(p, tx, slim_arms) + return p:get_properties().mesh..";"..tx..",blank.png,blank.png,blank.png" +end + +local db = minetest.get_mod_storage() +local world_dir = minetest.get_worldpath() +local meta_file = world_dir.."/libskinupload_meta.json" +local storage_dir = world_dir.."/libskinupload_skins/" +local storage_dir_meta = world_dir.."/libskinupload_meta/" +local optimize_media = minetest.settings:get_bool("libskinupload.optimize_media", true) +local use_fragmented_meta = minetest.settings:get_bool("libskinupload.fragmented_meta", false) +local allow_search = minetest.settings:get_bool("libskinupload.allow_search", true) and not use_fragmented_meta +minetest.mkdir(storage_dir) +if use_fragmented_meta then + minetest.mkdir(storage_dir_meta) +else + local m = io.open(meta_file) + local dl = minetest.get_dir_list(storage_dir_meta) + if m then + m:close() + elseif #dl > 0 then + minetest.log("action", "[ libskinupload ] Fragmented meta is disabled, but no master meta file was detected and a fragmented meta file is present. Automatically migrating to consolidated meta (this will not remove the libskinupload_meta folder)...") + local out = "{" + local i = 0 + for _, x in pairs(dl) do + minetest.log("action", "Merging file "..x.."...") + local id = x:match "libskinupload_uploaded_skin_(%d*).json" + if id then + local file = io.open(storage_dir_meta..x) + local data = file:read("a") + file:close() + out = out..(i == 0 and ("\""..id.."\":") or (",\""..id.."\":"))..data + i = i +1 + end + end + local f = io.open(meta_file, "w+") + f:write(out.."}") + f:flush() + f:close() + end +end + + +function libskinupload.add_skin_media(id, fname, cb) + local x + if not fname then + id = tostring(id) + x = "libskinupload_uploaded_skin_"..id..".png" + else + x = id + id = id:match "libskinupload_uploaded_skin_([^.]+).png" + end + if libskinupload.enabled[id] then return end + libskinupload.enabled[id] = true + if fname then + minetest.dynamic_add_media{ + filepath = storage_dir..x, + filename = x + } + else + minetest.dynamic_add_media({ + filepath = storage_dir..x, + filename = x + }, cb or function() end) + end + if mcl then + local meta = {} + if use_fragmented_meta then + local f = io.open(storage_dir_meta..string.gsub(x, ".png", ".json")) + if f then + meta = minetest.parse_json(f:read("a")) + f:close() + end + else + meta = libskinupload.get_skin_meta(x) + end + if not meta.p then + mcl_skins.register_simple_skin{texture = x, slim_arms = meta.msa or false} + end + end +end + +if not optimize_media then + for _, x in ipairs(minetest.get_dir_list(storage_dir, false)) do + libskinupload.add_skin_media(x, true) + end +end + +minetest.register_privilege("skin_review", { + on_grant = function(name) + libskinupload.reviewers[name] = true + db:set_string("reviewers", minetest.serialize(libskinupload.reviewers)) + end, + on_revoke = function(name) + libskinupload.reviewers[name] = nil + db:set_string("reviewers", minetest.serialize(libskinupload.reviewers)) + end, + give_to_singleplayer = false, + give_to_admin = false +}) + +libskinupload.notifications = minetest.deserialize(db:get_string("notifications")) or {} +libskinupload.reviewers = minetest.deserialize(db:get_string("reviewers")) or {} +libskinupload.requests = minetest.deserialize(db:get_string("requests")) or {} + +local function markdown(str) + local out = minetest.hypertext_escape(str) + :gsub("__(.+)__", "%1") + :gsub("%*%*(.+)%*%*", "%1") + :gsub("%^%^(.+)%^%^", "%1") + :gsub("``(.+)``", "%1") + return out +end + +local function listify(name) + local out = {} + for _, x in pairs(libskinupload.requests[name]) do + out[#out +1] = x + end + libskinupload.requests[name] = out +end + +function libskinupload.notify(name, msg) + if name == ":reviewers" then + for n, _ in pairs(libskinupload.reviewers) do + if minetest.get_player_by_name(n) then + minetest.chat_send_player(n, msg) + end + end + else + if minetest.get_player_by_name(name) then + minetest.chat_send_player(name, msg) + else + libskinupload.notifications[name] = msg + db:set_string("notifications", minetest.serialize(libskinupload.notifications)) + end + end +end + +function libskinupload.get_skin_data(fname) + local f = io.open(storage_dir..fname) + local out = minetest.encode_base64(f:read("a")) + f:close() + return out +end + +minetest.register_on_joinplayer(function(p) + if db:contains("skin:"..p:get_player_name()) then + local skin = db:get_string("skin:"..p:get_player_name()) + libskinupload.set_skin(p, skin) + end + local msg = libskinupload.notifications[p:get_player_name()] + if msg then + minetest.chat_send_player(p:get_player_name(), msg) + libskinupload.notifications[p:get_player_name()] = nil + db:set_string("notifications", minetest.serialize(libskinupload.notifications)) + end + if minetest.check_player_privs(p, {skin_review = true}) then + local newskins = db:get_int("newrequests") + if newskins > 0 then minetest.chat_send_player(p:get_player_name(), minetest.colorize("#579a1e", "[libskinupload] "..newskins.." new skin request"..(newskins == 1 and " has" or "s have").." been submitted. Run /skinreview to see "..(newskins == 1 and "it" or "them")..".")) end + elseif p:get_player_name() == "singleplayer" then + minetest.change_player_privs(p:get_player_name(), {skin_review = true}) + end +end) + +function libskinupload.can_player_upload_skins(p) + if db:get("upload_permitted:"..p:get_player_name()) == "false" then + return false + end + return true +end + +function libskinupload.queue(p, req) + local data = req.data + if not libskinupload.can_player_upload_skins(p) then + minetest.chat_send_player(p:get_player_name(), "Insufficient permissions.") + return "No permission." + end + if not minetest.decode_base64(data) then + return "Invalid base64 data." + end + if #data < 5 or string.sub(data, 1, 5) ~= "iVBOR" then + return "Invalid PNG file." + end + local def = {name = req.name or "", desc = req.desc or "", data = data, uploader = p:get_player_name(), tags = req.tags} + if mcl then + def.mcl_slim_arms = req.mcl_slim_arms + end + def.private = req.private + local name = p:get_player_name() + local rq = libskinupload.requests[name] + if not rq then + libskinupload.requests[name] = {} + rq = libskinupload.requests[name] + end + local max = db:get_int("reqmax:"..name) + local num = #rq +1 + if max < 1 then max = 1 end + if num <= max then + rq[num] = "skinreq:"..name..num + db:set_string("requests", minetest.serialize(libskinupload.requests)) + db:set_string("skinreq:"..name..num, minetest.serialize(def)) + db:set_int("newrequests", db:get_int("newrequests") +1) + else + num = max + rq[num] = "skinreq:"..name..num + db:set_string("requests", minetest.serialize(libskinupload.requests)) + db:set_string("skinreq:"..name..num, minetest.serialize(def)) + end + if name == "singleplayer" then + libskinupload.accept(p, name.."@"..num) + return false + end + libskinupload.notify(":reviewers", minetest.colorize("#579a1e", "[libskinupload] A new skin request has been submitted. Run /skinreview to see it.")) + return false +end + +function libskinupload.deny(by, rq, msg) + if not minetest.check_player_privs(by, {skin_review = true}) then + minetest.chat_send_player(by:get_player_name(), "Insufficient permissions.") + return true + end + local num = rq:match "@(.+)" + rq = rq:gsub("@", "") + local name = minetest.deserialize(db:get_string("skinreq:"..rq)).uploader + minetest.log("action", by:get_player_name().." denied a skin upload request from "..name..". Reason: "..(msg or "")) + db:set_string("skinreq:"..rq, "") + libskinupload.requests[name][num] = nil + db:set_string("requests", minetest.serialize(libskinupload.requests)) + listify(name) + db:set_int("newrequests", db:get_int("newrequests") -1) + libskinupload.notify(name, minetest.colorize("#dc4207", "[libskinupload] Your skin request was denied. "..(msg and msg ~= "" and "Reason: "..msg or "No reason was provided."))) +end + +local function get_next_id() + local f, err = io.open(world_dir.."/libskinupload_nextid.txt", "r") + local out = 0 + if f then + out = tonumber(f:read("a")) + f:close() + end + f, err = io.open(world_dir.."/libskinupload_nextid.txt", "w") + local out2 = db:get_int("_nextid") + if out2 > out then out = out2 end + db:set_int("_nextid", out +1) + if f then + f:write(out +1) + f:flush() + f:close() + else + minetest.log("Notice: libskinupload failed to access nextid file: "..err.." Falling back to mod storage.") + end + return out +end +function libskinupload.accept(by, rq) + if not minetest.check_player_privs(by, {skin_review = true}) then + minetest.chat_send_player(by:get_player_name(), "Insufficient permissions.") + return true + end + local num = rq:match "@(.+)" + rq = rq:gsub("@", "") + local req = minetest.deserialize(db:get_string("skinreq:"..rq)) + local name = req.uploader + if name then + minetest.log("action", by:get_player_name().." accepted a skin upload request from "..name..".") + libskinupload.requests[name][num] = nil + db:set_string("requests", minetest.serialize(libskinupload.requests)) + listify(name) + end + local id = get_next_id() + local fp = storage_dir.."/libskinupload_uploaded_skin_"..id..".png" + if not minetest.safe_file_write(fp, minetest.decode_base64(req.data)) then minetest.log("error", "Failed to write skin file.") end + + if use_fragmented_meta then + local fpm = storage_dir_meta.."/libskinupload_uploaded_skin_"..id..".json" + local fm = io.open(fpm, "w+") + if not fm:write(minetest.write_json{n = req.name, d = req.desc, c = req.uploader or "", p = req.private or false, t = req.tags, msa = req.mcl_slim_arms or false}) then minetest.log("error", "Failed to write skin meta.") end + fm:flush() + fm:close() + else + local fm = io.open(meta_file, "r+") + if not fm then + io.open(meta_file, "w+"):close() + fm = io.open(meta_file, "r+") + end + if not fm:seek("end", -1) then + fm:write("{") + else + fm:write(",") + end + if not fm:write("\""..id.."\":"..minetest.write_json{n = req.name, d = req.desc, c = req.uploader or "", p = req.private or false, t = req.tags, msa = req.mcl_slim_arms or false}.."}") then minetest.log("error", "Failed to write skin meta.") end + fm:flush() + fm:close() + end + db:set_string("skinreq:"..rq, "") + db:set_int("newrequests", db:get_int("newrequests") -1) + if name then libskinupload.notify(name, minetest.colorize("#579a1e", "[libskinupload] Your skin request was accepted.")) end +end + +local upload_state = {} +function libskinupload.show_upload_dialog(name, args) + if not args or type(args) == "string" then args = {err = ""} end + local p = minetest.get_player_by_name(name) + if not libskinupload.can_player_upload_skins(p) then + minetest.chat_send_player(name, "Insufficient privileges.") + return + end + if not upload_state[name] then upload_state[name] = {} end + local m = upload_state[name] + local size = (minetest.get_player_window_information(name) or {}).max_formspec_size or {x = 20, y = 11.5} + local w = size.x /2 -1 + local w2 = size.x /2 -1.5 + local tags = "" + local x = 0 + local y = 0 + if m.tags then + for _, tag in ipairs(m.tags) do + local width = math.max(#tag *0.2, 1.5) + if x +width > w then + x = 0 + y = y +0.6 + end + tags = tags.."button["..x..","..y..";"..width..",0.5;remove_tag;"..tag.."]\ + " + x = x +width +0.05 + end + end + local start = math.max(y +1.35, (size.y -7) /2) + minetest.show_formspec(name, "libskinupload:upload", "formspec_version[7]\ + size["..size.x..","..size.y.."]\ + \ + "..((args.err and args.err ~= "") and "hypertext[0.5,"..(size.y -2)..";"..(size.x /3)..",3;;Error: "..args.err.."]" or "").."\ + container[0.5,0.5]\ + style[add_tag;bgcolor=#0000;border=false;bgimg=]\ + style[remove_tag;border=false;bgimg=libskinupload_tag_bg.png;bgimg_middle=8;padding=-8]\ + style[remove_tag:hovered;border=false;bgimg=libskinupload_tag_bg_hovered.png;bgimg_middle=8;padding=-8]\ + "..tags.."\ + "..(args.add_tag and "set_focus[new_tag,true]\ + field_close_on_enter[new_tag;false]\ + field["..x..","..y..";2,0.5;new_tag;;]" or "button["..x..","..y..";0.5,0.5;add_tag;+]").."\ + container_end[]\ + container[0.5,"..start.."]\ + field[0.5,0;"..w..",0.5;skin;Data;"..minetest.formspec_escape(m.data or "").."]\ + field[0.5,1;"..w..",0.5;skinname;Name;"..minetest.formspec_escape(m.name or "").."]\ + textarea[0.5,2;"..w..",2;skindesc;Description;"..minetest.formspec_escape(m.desc or "").."]\ + checkbox[0.5,4.5;private;Personal Skin;"..(not not m.private and "true" or "false").."]\ + ".. + (mcl and "checkbox[0.5,5;mcl_slim_arms;Slim Arms;"..(m.mcl_slim_arms and "true" or "false").."]" or "") + .."\ + button[0.5,6;"..w..",1;confirm;Upload Skin]\ + container_end[]\ + "..(m.data and m.data ~= "" and #m.data > 5 and string.sub(m.data, 1, 5) == "iVBOR" and minetest.decode_base64(m.data) and "model["..(size.x /2 +1)..",0.5;"..w2..","..(size.y -2)..";preview;"..player_model(p, m.data and minetest.formspec_escape("[png:"..m.data) or "blank.png", m.mcl_slim_arms)..";0,210;;true;;]" or "button["..(size.x /2 +1 +(w2 /4))..","..(size.y /2)..";"..(w2 /2)..",1;eh;Show Preview]\ + ").."\ + button["..(size.x /2 +1)..","..(size.y -1.5)..";"..w2..",1;help;Show Help]\ + "..(m.help and "box[0,0;"..size.x..","..size.y..";#000a]\ + style[help;bgcolor=#0000;border=false;bgimg=]\ + style[help:hovered;bgcolor=#0000;border=false;bgimg=]\ + style[help:pressed;bgcolor=#0000;border=false;bgimg=]\ + button[0,0;"..size.x..","..size.y..";help;]\ + hypertext["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2)..";;"..minetest.formspec_escape("To upload a skin, paste the base64-encoded image into the Data field below. This can be obtained from a terminal by running `base64 -i ` (e.g. `base64 -i ~/Desktop/skins/my_skin.png`) and copying the output. Alternatively, you can use a web tool (search 'base64-encode an image' or similar). Once a request is sbmitted, it will need to be reviewed. You will be notified when your request is accepted or denied. Note that you can only have one skin request pending approval at a time; if you send another request before the previous one is approved or rejected, the later request will overwrite the previous one.\nIf the preview for the skin you're uploading looks wrong, the image was probably in a layout that won't work with the player model. Usually, this is because you're trying to upload a Minecraft skin, which is incompatible with most Minetest player models. In that case, you can fix it by simply cropping off the bottom 32 pixels of the original 64x64 image and trying again.").."]" or "")) +end +minetest.register_chatcommand("skinupload", { + description = "Show the skin upload dialog.", + func = libskinupload.show_upload_dialog +}) + +local review_state = {} +function libskinupload.show_review_dialog(name, args) + local p = minetest.get_player_by_name(name) + local fs = "" + local fsp = [=[formspec_version[7] + size[12,10]]=] + if args == "" then + fs = "scroll_container[0.5,0.5;11,9;na;vertical;]" + + local i = 0 + for n, l in pairs(libskinupload.requests) do + for k, v in ipairs(l) do + local req = minetest.deserialize(db:get_string(v)) + if not req then goto continue end + if req.data and #req.data > 10923 then req.data = "" end + local pn = n.."@"..k + fs = fs.."style[view"..pn..";bgcolor=#0000;border=false]\ + style[view"..pn..":hovered;bgcolor=#0001;border=false]\ + style[view"..pn..":focused;bgcolor=#0001;border=false]\ + style[view"..pn..":pressed;bgcolor=#0001;border=false]\ + model["..((i %4) *2)..","..4 *math.floor(i /4)..";2,4;pre;"..player_model(p, "\\[png:"..minetest.formspec_escape(req.data), req.mcl_slim_arms or false)..";0,210;;false;0,0;]\ + button["..((i %4) *2)..","..4 *math.floor(i /4)..";2,4;view"..pn..";]" + i = i + 1 + ::continue:: + end + end + if i == 0 then + db:set_int("newrequests", 0) + fs = fs.."\ + hypertext[-0.5,-0.5;12,10;;There are no pending skin requests.]" + end + fs = fsp..(i > 8 and "\ + scrollbaroptions[min=0;max="..(50 *(math.ceil(i /4) - 2))..";thumbsize=10]\ + scrollbar[10.8,0.5;0.2,9;vertical;na;0]\ + " or "")..fs..[=[ + scroll_container_end[] + ]=] + else + local m = review_state[name] + local req = m.req + if not req then + m.req = minetest.deserialize(db:get_string(libskinupload.requests[args:match "^([^@]+)@"][tonumber(args:match "@(.+)")])) + req = m.req + end + local tags = "style[remove_tag;border=false;bgimg=libskinupload_tag_bg.png;bgimg_middle=8;padding=-8]\ + style[remove_tag:hovered;border=false;bgimg=libskinupload_tag_bg_hovered.png;bgimg_middle=8;padding=-8]\ + " + local x = 0 + local y = 0 + if req.tags then + for _, tag in ipairs(req.tags) do + local width = math.max(#tag *0.2, 1.5) +-- if x +width > 10 then +-- x = 0 +-- y = y +0.6 +-- end + tags = tags.."button["..x..","..y..";"..width..",0.5;remove_tag;"..tag.."]\ + " + x = x +width +0.05 + end + end + fs = "formspec_version[7]\ + size[12,12]\ + model[0.5,0.5;8,7;pre;"..player_model(p, "\\[png:"..minetest.formspec_escape(req.data), req.mcl_slim_arms or false)..";0,210;;true;0,0;]\ + button[8,1;3,1;accept;Accept]\ + button[8,2;3,1;showdeny;Deny]\ + label[8,3.5;Creator\\: "..(req.uploader or "N/A").."]\ + hypertext[8,4;3,2;;Name\\: "..minetest.formspec_escape(minetest.hypertext_escape(req.name or "N/A")).."]\ + hypertext[0.5,8.5;8,3;;"..minetest.formspec_escape(markdown(req.desc) or "N/A").."]\ + button[8,6;3,1;back;Back]\ + scroll_container[0.5,7.5;11,1;tagscroll;horizontal;;0]\ + scrollbar[0,-800;8,0;horizontal;tagscroll;0]\ + "..tags.."\ + "..(m.add_tag and "set_focus[new_tag,true]\ + field_close_on_enter[new_tag;false]\ + field["..x..",0;2,0.5;new_tag;;]" or "button["..x..",0;0.5,0.5;add_tag;+]").."\ + scroll_container_end[]" + if m.reason then + fs = fs.."\ + box[0,0;12,12;#000a]\ + style[closedeny;bgcolor=#0000;border=false;bgimg=]\ + style[closedeny:hovered;bgcolor=#0000;border=false;bgimg=]\ + style[closedeny:pressed;bgcolor=#0000;border=false;bgimg=]\ + button[0,0;12,12;closedeny;]\ + textarea[3,2.5;6,5;reason;Reason;"..m.reason.."]\ + button[3,7.5;3,1;closedeny2;Cancel]\ + button[6,7.5;3,1;deny;Deny]\ + " + end + end + minetest.show_formspec(name, "libskinupload:review", fs) +end +minetest.register_chatcommand("skinreview", { + description = "Show the skin review dialog.", + func = function(name, args) + review_state[name] = {} + libskinupload.show_review_dialog(name, "") + end, + privs = {skin_review = true} +}) + +local function pattern_escape(str) + return str:gsub("([]%)([.^$*+-])", "%%%1") +end + +local choose_state = {} +function libskinupload.show_choose_dialog(name) + local p = minetest.get_player_by_name(name) + local m = choose_state[name] + local target = m.current + local search = m.search + local fs = "" + local fsp = [=[formspec_version[7] + size[12,10]]=] + + if not choose_state["@dirlist"] or not #choose_state["@dirlist"] then + choose_state["@dirlist"] = minetest.get_dir_list(storage_dir, false) + choose_state["@max_pages"] = math.floor(#choose_state["@dirlist"] /8) + end + if not choose_state["@index"] then + local f = io.open(meta_file) + if not f then + choose_state["@index"] = {} + else + local res = f:read("a") + choose_state["@index"] = minetest.parse_json(res) + f:close() + end + end + if target == "" or not target then + fs = fs.."container[2,0.5]" + + if search and allow_search then + local out = "\ + " + + local tags = {} + for x in search:gmatch("%[([^%]]+)%]") do + tags[x] = 1 + end + search = search:gsub("%[[^%]]+%]", ""):gsub("%s+$", "") + + --minetest.chat_send_player(name, minetest.serialize{search, tags}) + + local i = 0 + local j = 0 + for id, meta in pairs(choose_state["@index"]) do + if not (next(tags) and search == "") and not meta.n:lower():find(search:lower(), nil, true) or meta.p and meta.c ~= name then + goto continue + else + if next(tags) then + if not meta.t then goto continue end + for _, x in pairs(meta.t) do + if tags[x] then + tags[x] = 2 + end + end + for _, x in pairs(tags) do + if x < 2 then goto continue end + end + end + if j < m.page *8 or j >= m.page *8 +8 then + j = j +1 + goto continue + end + end + local pn = "libskinupload_uploaded_skin_"..id..".png" + local private = meta.p and meta.c ~= name + out = out.."style[view"..pn..";bgcolor=#0000;border=false]\ + style[view"..pn..":hovered;bgcolor=#0001;border=false]\ + style[view"..pn..":focused;bgcolor=#0001;border=false]\ + style[view"..pn..":pressed;bgcolor=#0001;border=false]\ + "..(private and + "model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, "[fill:1x1:#777", not not meta.msa)..";0,210;;false;0,0;]\ + image["..((i %4) *2 +0.75)..","..(4 *math.floor(i / 4) +1.75)..";0.5,0.5;libskinupload_locked.png;]" or + "model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, optimize_media and "\\[png:"..libskinupload.get_skin_data(pn) or pn, not not meta.msa)..";0,210;;false;0,0;]\ + button["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;view"..pn..";]") + i = i +1 + j = j +1 + ::continue:: + end + if i == 0 then + out = out.."\ + hypertext[-2,-0.5;12,8;;No matches found.]" + end + m.max_pages = math.ceil(j /8) -1 + fs = fsp..fs..out.."\ + container_end[]\ + label[0.5,8.5;Page "..m.page.."]\ + "..(allow_search and "style[searchfor;noclip=true]\ + style[search;noclip=true;border=false]\ + image_button[1,-1;1,1;libskinupload_search.png;search;]\ + field[2,-0.75;8,0.5;searchfor;;"..minetest.formspec_escape(m.search or "").."]\ + tooltip[search;Search;#444;#aaa]\ + field_close_on_enter[searchfor;false]\ + " or "").."\ + button[0.5,9;1,0.5;prev;<<]\ + scrollbaroptions[min=0;max="..m.max_pages..";smallstep=1;largestep=10;arrows=hide]\ + scrollbar[1.5,9;9,0.5;horizontal;page;"..m.page.."]\ + button[10.5,9;1,0.5;next;>>]\ + " + minetest.show_formspec(name, "libskinupload:choose", fs) + return +-- fs = fs.."\ +-- hypertext[-2,-0.5;12,8;;Searching...]" + else + local i = 0 + while true do + local pn = choose_state["@dirlist"][i +(m.page *8) +1] + if not pn then break end + local meta = libskinupload.get_skin_meta(pn) or {p = false, msa = false, c = ""} + --if not meta then break end + local private = meta.p and meta.c ~= name + fs = fs.."style[view"..pn..";bgcolor=#0000;border=false]\ + style[view"..pn..":hovered;bgcolor=#0001;border=false]\ + style[view"..pn..":focused;bgcolor=#0001;border=false]\ + style[view"..pn..":pressed;bgcolor=#0001;border=false]\ + "..(private and + "model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, "[fill:1x1:#777", not not meta.msa)..";0,210;;false;0,0;]\ + image["..((i %4) *2 +0.75)..","..(4 *math.floor(i / 4) +1.75)..";0.5,0.5;libskinupload_locked.png;]" or + "model["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;pre;"..player_model(p, optimize_media and "\\[png:"..libskinupload.get_skin_data(pn) or pn, not not meta.msa)..";0,210;;false;0,0;]\ + button["..((i %4) *2)..","..4 *math.floor(i / 4)..";2,4;view"..pn..";]") + i = i +1 + if i >= 8 then break end + end + if i == 0 then + fs = fs.."\ + hypertext[-2,-0.5;12,8;;No skins have been uploaded yet.]" + end + end + + fs = fsp..fs.."\ + container_end[]\ + label[0.5,8.5;Page "..m.page.."]\ + "..(allow_search and "style[searchfor;noclip=true]\ + style[search;noclip=true;border=false]\ + image_button[1,-1;1,1;libskinupload_search.png;search;]\ + field[2,-0.75;8,0.5;searchfor;;"..minetest.formspec_escape(m.search or "").."]\ + tooltip[search;Search;#444;#aaa]\ + field_close_on_enter[searchfor;false]\ + " or "").."\ + button[0.5,9;1,0.5;prev;<<]\ + scrollbaroptions[min=0;max="..choose_state["@max_pages"]..";smallstep=1;largestep=10;arrows=hide]\ + scrollbar[1.5,9;9,0.5;horizontal;page;"..m.page.."]\ + button[10.5,9;1,0.5;next;>>]\ + " + else + local id = string.gsub(string.sub(target, string.len("libskinupload_uploaded_skin_."), -1), ".png", "") + local meta = choose_state["@index"][id] --libskinupload.get_skin_meta(target) + if not meta then + meta = {p = false} + end + if meta.p and meta.c ~= name then + fs = fs.."label[0.5,0.5;Insufficient privileges.]" + return + end + local tags = "style[remove_tag;border=false;bgimg=libskinupload_tag_bg.png;bgimg_middle=8;padding=-8]\ + style[remove_tag:hovered;border=false;bgimg=libskinupload_tag_bg_hovered.png;bgimg_middle=8;padding=-8]\ + " + local x = 0 + local y = 0 + if meta.t then + for _, tag in ipairs(meta.t) do + local width = math.max(#tag *0.2, 1.5) +-- if x +width > 10 then +-- x = 0 +-- y = y +0.6 +-- end + tags = tags.."button["..x..","..y..";"..width..",0.5;remove_tag;"..tag.."]\ + " + x = x +width +0.05 + end + end + fs = fsp.."model[0.5,0.5;8,7;pre;"..player_model(p, optimize_media and "\\[png:"..libskinupload.get_skin_data(target) or target, not not meta.msa)..";0,210;;true;0,0;]\ + label[8,1;"..minetest.formspec_escape("ID: "..string.gsub(string.sub(target, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")).."]\ + button[8,2;3,1;confirm;Set Skin]\ + hypertext[8,3.5;3,2;;"..minetest.formspec_escape(minetest.hypertext_escape(meta.n or "N/A")).."]\ + hypertext[0.5,8;8,2;;"..minetest.formspec_escape(markdown(meta.d or "N/A")).."]\ + scroll_container[0.5,7.5;11,1;tagscroll;horizontal;;0]\ + scrollbar[0,-800;8,0;horizontal;tagscroll;0]\ + "..tags.."\ + scroll_container_end[]\ + button[8,6;3,1;back;Back]" + end + minetest.show_formspec(name, "libskinupload:choose", fs) +end +minetest.register_chatcommand("skinchoose", { + description = "Show the skin selection dialog.", + func = function(name, args) + choose_state[name] = {page = 0} + libskinupload.show_choose_dialog(name, "") + end +}) + +minetest.register_chatcommand("skinchange", { + params = "", + description = "Set your skin directly by ID, bypassing the skin selection dialog.", + func = function(name, args) + local f = io.open(storage_dir.."libskinupload_uploaded_skin_"..args..".png") + if not f then + minetest.chat_send_player(name, "Invalid identifier.") + return + else + f:close() + end + local meta = libskinupload.get_skin_meta("libskinupload_uploaded_skin_"..args..".png") + if meta.p then + minetest.chat_send_player(name, "Insufficient privileges.") + return + end + libskinupload.set_skin(minetest.get_player_by_name(name), args) + end +}) + +minetest.register_chatcommand("skindelete", { + params = "", + description = "Remove the target skin from the filesystem.", + func = function(name, id) + local meta = {} + if use_fragmented_meta then + local f = io.open(storage_dir_meta.."libskinupload_uploaded_skin_"..id..".json") + if not f then + minetest.chat_send_player(name, "Invalid identifier.") + return + end + meta = minetest.parse_json(f:read("a")) or {} + f:close() + os.remove(storage_dir_meta.."libskinupload_uploaded_skin_"..id..".json") + else + local f = io.open(meta_file) + res = minetest.parse_json(f:read("a")) + f:close() + meta = res[tostring(id)] + res[tostring(id)] = nil + f = io.open(meta_file, "w+") + f:write(minetest.write_json(res)) + f:flush() + f:close() + end + os.remove(storage_dir.."libskinupload_uploaded_skin_"..id..".png") + minetest.chat_send_player(name, "Successfully deleted skin '"..(meta.n or "#"..id).."' from "..(meta.c or "[unknown]")..".") + end, + privs = {skin_review = true} +}) + +minetest.register_chatcommand("skinlimit", { + privs = {skin_review = true}, + params = " ", + description = "Set the maximum concurrent skin requests allowed for player to .", + func = function(name, args) + local pn, num = args:match "([%w_-]+)%s+(%d+)" + num = tonumber(num) + if pn and pn ~= "" and num and num > 0 then + db:set_int("reqmax:"..pn, num) + minetest.chat_send_player(name, "Set "..pn.."'s max concurrent requests to "..num..".") + else + minetest.chat_send_player(name, "Invalid parameters.") + end + end +}) + +minetest.register_chatcommand("skinforget", { + description = "Forget your curent skin, to prevent conflicts when you change your skin to one provided by another mod.", + func = function(name, args) + libskinupload.forget_skin(name) + end +}) + +function libskinupload.print_skin_info(name, skin) + local m = libskinupload.get_skin_meta(skin) + if not m then + minetest.chat_send_player(name, "Skin `"..skin.."` does not have metadata!") + return + end + minetest.chat_send_player(name, "Info for skin "..skin..":\ + Name: "..m.n.."\ + Description: "..m.d.."\ + Created by: "..m.c.."\ + Tags: "..minetest.write_json(m.t)) +end + +minetest.register_chatcommand("skinmanage", { + privs = {skin_review = true}, + params = "cull | list | meta | grant | revoke | privs | alter =[,=]*", + func = function(name, args) + if args == "cull" then + local i = 0 + for _, x in pairs(minetest.get_dir_list(storage_dir)) do + if not libskinupload.get_skin_meta(x) then + os.remove(storage_dir..x) + minetest.chat_send_player(name, "The skin `"..x.."` was removed because it did not have a matching meta file.") + i = i +1 + end + end + elseif args:match "^meta" then + libskinupload.print_skin_info(name, "libskinupload_uploaded_skin_"..args:gsub("^meta%s", "")..".png") + elseif args:match "^alter" then + local id, changes = args:match "^alter%s+(%d+)%s+(.+)" + local fname = "libskinupload_uploaded_skin_"..id..".png" + local m = libskinupload.get_skin_meta(fname) + for k, v in changes:gmatch "(%w+)%s*=%s*([^,=]+)" do + if k == "private" or k == "p" then + k = "p" + v = v == "true" + elseif k == "creator" then + k = "c" + elseif k == "description" then + k = "d" + elseif k == "name" then + k = "n" + elseif k == "t" or k == "tags" then + k = "t" + v = v:split(",") + end + m[k] = v + end + libskinupload.set_skin_meta(fname, m) + elseif args:match "^tag" then + local id, tags = args:match "^tag%s+(%d+)%s+(.+)" + local fname = "libskinupload_uploaded_skin_"..id..".png" + local m = libskinupload.get_skin_meta(fname) + if not m.t then m.t = {} end + tags = tags:split(",") + for _, x in pairs(tags) do + table.insert(m.t, x) + end + libskinupload.set_skin_meta(fname, m) + elseif args:match "^untag" then + local id, tags = args:match "^untag%s+(%d+)%s+(.+)" + local fname = "libskinupload_uploaded_skin_"..id..".png" + local m = libskinupload.get_skin_meta(fname) + if not m.t then return end + tags = tags:split(",") + local itags = {} + for _, x in pairs(tags) do + itags[x] = true + end + local rm = {} + for i, x in pairs(m.t) do + if itags[x] then rm[i] = true end + end + for k in pairs(rm) do + table.remove(m.t, k) + end + libskinupload.set_skin_meta(fname, m) + elseif args:match "^list" then + for _, x in pairs(minetest.get_dir_list(storage_dir)) do + libskinupload.print_skin_info(name, x) + end + elseif args:match "^remove_fragmented_meta" then + minetest.rmdir(storage_dir_meta, true) + minetest.chat_send_player(name, "Removed all fragmented meta.") + elseif args:match "^grant" then + local pname = args:match "grant%s+([%w_-]+)" + db:set_string("upload_permitted:"..pname, "") + minetest.chat_send_player(name, "Granted skin upload privileges to player "..pname..".") + elseif args:match "^revoke" then + local pname = args:match "revoke%s+([%w_-]+)" + db:set_string("upload_permitted:"..pname, "false") + minetest.chat_send_player(name, "Revoked skin upload privileges from player "..pname..".") + elseif args:match "^privs" then + local pname = args:match "privs%s+([%w_-]+)" + minetest.chat_send_player(name, "Player "..pname.." is "..(db:get("upload_permitted:"..pname) == "false" and "not " or "").."allowed to upload skins.") + else + minetest.chat_send_player(name, "Invalid arguments, see /help skinmanage for accepted subcommands.") + end + end, + description = "Print debug information about libskinupload. /skinmanage meta : Print metadata for the specified skin. Does not validate the provided ID! /skinmanage alter: Change the metadata of a skin. /skinmanage cull: Remove unmatched skin or meta files." +}) + +local function apply_skin(p, tx, id) + p:set_properties({ + textures = tx, + }) + if minetest.get_modpath("u_skins") then + u_skins.u_skins[p:get_player_name()] = "libskinupload_uploaded_skin_"..id + u_skins.save() + elseif mcl then + mcl_player.player_set_skin(p, "libskinupload_uploaded_skin_"..id..".png") + mcl_skins.save(p) + end + if minetest.get_modpath("3d_armor") then + armor.textures[p:get_player_name()].skin = "libskinupload_uploaded_skin_"..id..".png" + end +end + +function libskinupload.set_skin(p, id) + local fname = "libskinupload_uploaded_skin_"..id..".png" + local meta = libskinupload.get_skin_meta(fname) + if not meta then + libskinupload.forget_skin(p) + minetest.chat_send_player(p:get_player_name(), "Your skin does not exist anymore.") + return + end + if meta.p and meta.c ~= p:get_player_name() then + --minetest.chat_send_player(p:get_player_name(), "Insufficient permissions.") + return + end + local m = p:get_meta() + local tx = p:get_properties().textures + tx[1] = fname..m:get_string("texmod") + if not libskinupload.enabled[id] then + libskinupload.add_skin_media(id, false, function(name) + -- 5s delay to dodge a potential race condition that happens for unknown reasons. + minetest.after(5, function() + apply_skin(p, tx, id) + end) + end) + else + minetest.after(0, function() + apply_skin(p, tx, id) + end) + end + + db:set_string("skin:"..p:get_player_name(), id) +end + +function libskinupload.forget_skin(p) + db:set_string("skin:"..(type(p) == "string" and p or p:get_player_name()), "") +end + +function libskinupload.get_skin_id(name) + return db:get_string("skin:"..name) +end + +function libskinupload.get_skin(name) + local id = db:get("skin:"..name) + return id and "libskinupload_uploaded_skin_"..id..".png" +end + +function libskinupload.get_skin_meta(skin) + if use_fragmented_meta then + local f = io.open(storage_dir_meta..string.gsub(skin, ".png", ".json")) + local meta + if f then + meta = minetest.parse_json(f:read("a")) + f:close() + return meta + else + return false + end + else + if choose_state["@index"] then + return choose_state["@index"][string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] or {n = "Unnamed", d = "Nondescript", c = ""} + end + local f = io.open(meta_file) + if not f then + return false + else + local res = minetest.parse_json(f:read("a")) + f:close() + if not res then return {n = "Unnamed", d = "Nondescript", c = ""} end + return res[string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] or {n = "Unnamed", d = "Nondescript", c = ""} + end + end +end + +function libskinupload.set_skin_meta(skin, meta) + if use_fragmented_meta then + local f = io.open(storage_dir_meta..string.gsub(skin, ".png", ".json"), "w") + f:write(minetest.write_json(meta)) + f:close() + else + local f = io.open(meta_file) + if not f then + return false + else + local res = minetest.parse_json(f:read("a")) + f:close() + res[string.gsub(string.sub(skin, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")] = meta + f = io.open(meta_file, "w+") + f:write(minetest.write_json(res)) + f:flush() + f:close() + end + end +end + +minetest.register_chatcommand("skinget", { + params = "", + description = "Print the ID of the skin currently worn by the target player.", + func = function(name, args) + if args == "" then args = name end + local p = minetest.get_player_by_name(args) + if p then + local id = libskinupload.get_skin_id(args) + if id == "" then id = "an external skin." else id = "skin #"..id.."." end + minetest.chat_send_player(name, "Player `"..args.."` is wearing "..id) + end + end +}) + +--Compat +if minetest.get_modpath("unified_inventory") then + unified_inventory.register_page("libskinupload", { + get_formspec = function(p) + local name = p:get_player_name() + local tx = p:get_properties().textures[1] + local fs = (unified_inventory.style_full and unified_inventory.style_full.standard_inv_bg or "background[0.06,0.99;7.92,7.52;ui_misc_form.png]").."\ + model[0.5,0.75;1,2;pre;character.b3d;"..tx..";0,210;;false;0,0;]" + local meta = {} + if string.find(tx, "libskinupload_uploaded_skin_", 1, true) then + if use_fragmented_media then + local id = string.gsub(string.sub(tx, string.len("libskinupload_uploaded_skin_."), -1), ".png", "") + local f = io.open(storage_dir_meta.."libskinupload_uploaded_skin_"..id..".json") + if f then + meta = minetest.parse_json(f:read("a")) + f:close() + end + else + meta = libskinupload.get_skin_meta(tx) + end + end + fs = fs .. "label[2,.5;Name: "..minetest.formspec_escape(meta.n or "N/A").."]\ + textarea[2,1;6,2;;;"..minetest.formspec_escape(meta.d or "N/A").."]" + + fs = fs.."button[.75,3;4.5,.5;libskinupload_chooser;Change]\ + button[5.25,3;2,.5;libskinupload_upload_trigger;Upload Skin]" + return {formspec = fs} + end, + }) + + unified_inventory.register_button("libskinupload", { + type = "image", + image = "libskinupload_icon.png", + tooltip = "Choose Player-Submitted Skin", + }) +end + +minetest.register_on_player_receive_fields(function(p, name, data) + if name == "libskinupload:upload" then + if not libskinupload.can_player_upload_skins(p) then + minetest.chat_send_player(p:get_player_name(), "Insufficient permissions.") + minetest.close_formspec(p:get_player_name(), name) + return true + end + local m = upload_state[p:get_player_name()] + if data.skin then + if #data.skin >= 10923 then data.skin = string.sub(data.skin, 1, 10923) end + m.data = data.skin:gsub("^data:image/png;base64,", ""):gsub("[^A-Za-z0-9+/=]", "") + end + if data.key_enter_field == "new_tag" then + if not m.tags then m.tags = {} end + if #m.tags >= 50 then + libskinupload.show_upload_dialog(p:get_player_name(), {err = "Skins may not have more than 50 tags."}) + return true + end + if #data.new_tag >= 51 then data.new_tag = string.sub(data.new_tag, 1, 51) end + m.tags[#m.tags +1] = data.new_tag:lower():gsub("[^a-z_0-9$-]", "") + libskinupload.show_upload_dialog(p:get_player_name()) + return true + end + if data.remove_tag and m.tags then + local i = 0 + for k, v in pairs(m.tags) do + if v == data.remove_tag then + i = k + break + end + end + table.remove(m.tags, i) + libskinupload.show_upload_dialog(p:get_player_name()) + return true + end + if data.key_enter_field or data.confirm then + if mcl then + data.mcl_slim_arms = m.mcl_slim_arms or false + end + data.private = m.private or false + local x = libskinupload.queue(p, m) + if x then + libskinupload.show_upload_dialog(p:get_player_name(), {err = x}) + return true + end + upload_state[p:get_player_name()] = nil + minetest.close_formspec(p:get_player_name(), name) + return true + end + if data.quit then + upload_state[p:get_player_name()] = nil + return true + end + if data.private then + m.private = data.private + end + if data.mcl_slim_arms then + m.mcl_slim_arms = data.mcl_slim_arms == "true" + end + if data.help then + m.help = not m.help + end + m.name = data.skinname + m.desc = data.skindesc + if data.add_tag then + libskinupload.show_upload_dialog(p:get_player_name(), {add_tag = true}) + return true + end + libskinupload.show_upload_dialog(p:get_player_name()) + return true + elseif name == "libskinupload:review" then + local name = p:get_player_name() + if not minetest.check_player_privs(p, {skin_review = true}) then + minetest.chat_send_player(name, "Insufficient permissions.") + return true + end + if data.quit then + review_state[name] = nil + return true + end + local m = review_state[name] + if data.back then + m.req = nil + m.current = nil + m.add_tag = nil + libskinupload.show_review_dialog(name, "") + return true + elseif data.showdeny then + review_state[name].reason = "" + libskinupload.show_review_dialog(name, review_state[name].current) + elseif data.closedeny or data.closedeny2 then + review_state[name].reason = nil + libskinupload.show_review_dialog(name, review_state[name].current) + elseif data.deny then + libskinupload.deny(p, review_state[name].current, data.reason) + libskinupload.show_review_dialog(name, "") + return true + elseif data.accept then + db:set_string("skinreq:"..m.current:gsub("@", ""), minetest.serialize(m.req)) + libskinupload.accept(p, review_state[name].current) + libskinupload.show_review_dialog(name, "") + return true + end + if data.key_enter_field == "new_tag" then + if not m.req.tags then m.req.tags = {} end + if #m.req.tags >= 50 then + return true + end + if #data.new_tag >= 51 then data.new_tag = string.sub(data.new_tag, 1, 51) end + m.req.tags[#m.req.tags +1] = data.new_tag:lower():gsub("[^a-z_0-9$-]", "") + m.add_tag = nil + libskinupload.show_review_dialog(name, review_state[name].current) + return true + end + if data.remove_tag then + if not m.req.tags then m.req.tags = {} end + if #m.req.tags < 1 then + return true + end + local idx = 0 + for i, x in pairs(m.req.tags) do + if x == data.remove_tag then + idx = i + break + end + end + table.remove(m.req.tags, idx) + m.add_tag = nil + libskinupload.show_review_dialog(name, review_state[name].current) + return true + end + if data.add_tag then + m.add_tag = true + libskinupload.show_review_dialog(name, review_state[name].current) + return true + end + for k, v in pairs(data) do + if string.find(k, "view", 1, true) then + review_state[p:get_player_name()].current = string.gsub(k, "^view", "") + libskinupload.show_review_dialog(name, review_state[name].current) + return true + end + end + return true + elseif name == "libskinupload:choose" then + if data.quit then + choose_state[p:get_player_name()] = nil + choose_state["@dirlist"] = nil + choose_state["@index"] = nil + return true + end + local m = choose_state[p:get_player_name()] + if data.back then + m.current = "" + libskinupload.show_choose_dialog(p:get_player_name()) + return true + elseif data.confirm then + libskinupload.set_skin(p, string.gsub(string.sub(m.current, string.len("libskinupload_uploaded_skin_."), -1), ".png", "")) + minetest.close_formspec(p:get_player_name(), name) + return true + end + if allow_search and (data.search or data.key_enter_field == "searchfor") then + if #data.searchfor > 2 then + m.search = data.searchfor + else + m.search = nil + m.max_pages = nil + end + m.page = 0 + libskinupload.show_choose_dialog(p:get_player_name()) + return true + end + for k, v in pairs(data) do + if string.find(k, "view", 1, true) then + m.current = string.gsub(k, "^view", "") + libskinupload.show_choose_dialog(p:get_player_name()) + return true + end + end + if data.page then + local ev = minetest.explode_scrollbar_event(data.page) + if ev.type == "CHG" then + m.page = ev.value + libskinupload.show_choose_dialog(p:get_player_name()) + end + end + if data.prev then + m.page = m.page -1 + if m.page < 0 then + m.page = m.max_pages or choose_state["@max_pages"] + end + libskinupload.show_choose_dialog(p:get_player_name()) + elseif data.next then + m.page = m.page +1 + if m.page > (m.max_pages or choose_state["@max_pages"]) then + m.page = 0 + end + libskinupload.show_choose_dialog(p:get_player_name()) + end + return true + elseif data.libskinupload_chooser then + choose_state[p:get_player_name()] = {page = 0} + libskinupload.show_choose_dialog(p:get_player_name()) + elseif data.libskinupload_upload_trigger then + libskinupload.show_upload_dialog(p:get_player_name()) + end +end) + + +--[[ +minetest.register_on_joinplayer(function(p) +for i = 1, 100 do + local rd = "" + for i = 1,10 do + rd = rd..string.char(math.floor(math.random() *2) +97) + end + db:set_string("skinreq:a"..i, minetest.serialize{name = "",desc = rd, data = "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAGIklEQVRogdWYXWwcVxXHf3fmzox37CWxg6II/BTxJbWxylcTJFphkbqooTYFHirRqragtJECVFGQSqO2akWBB1RRsFQ/kbZ8hErQikURgog6SislKX3BRYgSKZGQ/ZIoa5Y43tmZO/fyMJ7Jfnl3Fq8d5S9Zvufcjzn/e88599wVdMFvfzxhAMJQNehdVwKwshp2nD/91CnR7Rs3EjLvQGUah7p9N+XGIPcGSKGaNX025cYgNwttLDQWUii0sVr6p8YfwrZtAOI45vfzL/fPyk1EK5N1kJIHsITeNIO2Gj35cX0eaM4BsTaItY2JtdmwYVuFvuWAcHWV2PNAWGh983iI9cRrt+zOM7A+7ptzwPhtU2A0uhagowATVRn/+L39tXSTIEo/mdhUfzWmsQwQovFzk4/9OVedcOKndxuAS8tJ3THz9Hyueafm582esTEAduzYIQCuXLliAN5dWEAGneuYPuA6Yc8RGCOoRb3t+bFnxg2A6/nsHE42IdV124g9Y2O8u7DA8MhIpltcXGS5XGbP2BhSqebY7i/SfOi5NqFKBCGgFsa55qdELy2HjO7yGzYhL4ZHRlgulxtOPt2QPlQznRNeNYgpDDjUwhjPTeqEvOTrMbpre9buZRPq3b5dO9cGPPLDv3L16lX0318DwLr1yxSLRX723U/iym6lREw1iLJNSFENojyfBmDnsEtYW2VgcDuf//qrmcuneSEPFhcX27atARc6/R2Z/SdLS0v4D58HFYKw8B8+z9LSEk/M/avj3IG6YqGecC/kITlp1/MbyAMc+Pafcj+0lsvltu2uSXD28McoFov4wzZR9DaWifCHP0Cx6PODRz9Ct/mDBYdr1YRwM/HBgpPL+NFd27nrkdc39Kr83Pi4aNfOFQLLJ1/g4t0CdU2Csbh44Jf851T35BmqJD84jk0UNca949hZfyfMPD0v0kS4GRB3vv7RgdP3vRekCtu2Gz525y07GibML1zqeBLdjP3Gs6c7GhTHccf1/31i1jiuDUYThTXCIMS2BQiJikM+/JXv9eQpMrzvvZvqXeu4No4tQNsYx0XH4JoQ7TgI0XuU3FTkM6xFjtAGhEZL//9eSp6FlT6Z1YL7J6bBaH5z8pW+rRlFUfbqjGoKFQUYJzn5WPV2uwCI5pjvFRePPwmAEUmR88Y/3sj67r39AAB/ePtEptv/iS+C0YACY/GXv/0RSG4EFSVXykpV4zjJevtv/yqWldQaRsdgSQQGVIhWITpWWEohTEy89u6Q29+fvUpHv3CoY1xsOAQe/M6PGuRDh+9gZTW5IWThfQ19Q77kgW8ebdDNHNzXsmZKHuBrM0c2amJH9CUHDHuJwcu15Kob8iUrq4rzT/4iGbBfMuTLdcevh7ROaB7fq9wJ8r+nf44FCFsQawGxSlzOaLTSaLWKwCAQaCmxvSJC1cACrTT33P+tlkU9RzA7d4ZTn7oLgCNzJ3n86HhbA469eJaZg/u4Vo0YLLioKMSThpoSzD7/ZlcCG4XluQU818GVLp5fwHUknj+IJx08fxDHc/BsgetIHMdJ+h0bzy3geu0dyJF2W/1KtX3xdOzFswBZxZiiXXj0G5alFQz44EkspbBtm6mnXmbquVcRqoYtbGzHw3ZtbGEQqsbUc79j6tnj2LLQdtGVqmo48cePjq9LHq4TzVsa9xNy6vvHKa89DkbW3sjlcplSqcSXpqczubkfYOqZX7UsOPv8mxw6fEeDLiXfzqXryae3QE0liTv1jM2ELNe9jNJ2qVRicnKyZXD92FRuFwTpLVCfY6/r8mEryANYQZA9AwiCgCAImJycpFQqZXJzf728HuoJdyLfjuhWkQewKpVKRqxSqVCpVCiVSkxMTGRyc3+93IzpRz+T/H9n6LrunSFemjuT9a0H6bjUlNiS5Jd98/Lly5mQtvfu3cu5c+fYvbvxF/P6san8wTZl+EtzZ7j1Q5/ObUR6FQ4WnOz3g5mD+zJPaL7Pe5U7QWzbtq2lFL5w4UIL+fVwm19r0b11qX1N/tmdnbN8/cmn5H/9ygtYjsSENXQYImwBIqlT4uAaGI0QAgtNrG2wLSzLQrgFjI4ZveexjqXw/wBU+sGWvrP/SgAAAABJRU5ErkJggg=="}) + libskinupload.accept(p, "a"..i) +end +end) +--]] \ No newline at end of file diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..098b513 --- /dev/null +++ b/mod.conf @@ -0,0 +1,2 @@ +name = libskinupload +optional_depends = unified_inventory, u_skins, mcl_skins, 3d_armor, nc_player_model \ No newline at end of file diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..93bf087 --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,2 @@ +libskinupload.optimize_media (Optimize media load) bool true +libskinupload.allow_search (Allow skin search) bool true \ No newline at end of file diff --git a/textures/libskinupload_icon.png b/textures/libskinupload_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..986c56d4b89af23fc7ba387377430ee72aa8988d GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|(mY)pLo9le zQw}hSZPESJf5v}BSMrf%eXP?Nd4S-!$XP|6lg1WT4IUadNHkn${gKng$Wy=Yx2)`f zcGfU`naxRU7rLGa@H}?TY+{?h#CDJ2)o~^sf!RVIs~F>72&Xa_CP+vyFf`5;x4HV` RPx$14%?dR5*>rlQC+;FcgMACl1K*65>cCI0a9IW+`McUULK=AVY7^Gxz}6LZQd# z6$(B`OG|^hwHT7(4k044) b0+*>@)eJ{wvPL$^00000NkvXXu0mjfD|ArY-_ zuN(3KRXGQK?2(yl{c1_3&z%sNI`f!c1&2=wmFK^{ebS}nrA_Bx=S!IiuFZQ2lJj)+b6Mw<&;$V5xH9Dc literal 0 HcmV?d00001 diff --git a/textures/libskinupload_tag_bg_hovered.png b/textures/libskinupload_tag_bg_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..ce84a8b0734cca02a3991871919c9738845359d0 GIT binary patch literal 155 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}p`I>|ArY-_ zuN(3KRXGQK?2(yl{c1_3&z%sNI`f!c1&2=wmFK^{ebS}nrA_Bx=S!IiuFZQ2lJj)+b6Mw<&;$V5xH9Dc literal 0 HcmV?d00001 diff --git a/uskins_import.php b/uskins_import.php new file mode 100644 index 0000000..5939674 --- /dev/null +++ b/uskins_import.php @@ -0,0 +1,65 @@ + $lastid) $lastid = $id; + if($lastid != 0) fwrite($file, ","); + fwrite($file, "\"$id\":"); + echo "Found skin ID $id:\n"; + echo " |- Copying image file...\n"; + copy($fname, "$world/libskinupload_skins/libskinupload_uploaded_skin_$id.png"); + if(file_exists("$uskins/meta/character_$id.txt")) { + echo " |- Migrating meta...\n"; + $meta = file_get_contents("$uskins/meta/character_$id.txt"); + preg_match('#author = "([^"]+)"#', $meta, $author); + preg_match('#name = "([^"]+)"#', $meta, $name); + preg_match('#comment = "([^"]+)"#', $meta, $comment); + if(!isset($author[1])) { + echo " |- Meta file seems invalid; falling back to default meta...\n"; + fwrite($file, json_encode([ + 'c' => '', + 'n' => 'Unnamed', + 'd' => '' + ])); + } else { + fwrite($file, json_encode([ + 'c' => $author[1], + 'n' => $name[1], + 'd' => $comment[1] + ])); + } + } else { + echo " |- Appending default meta...\n"; + fwrite($file, json_encode([ + 'c' => '', + 'n' => 'Unnamed', + 'd' => '' + ])); + } + } +} +++$lastid; + +fwrite($file, "}"); + +fclose($file); + +echo "Transfer complete, incrementing internal counter...\n"; + +file_put_contents("$world/libskinupload_nextid.txt", $lastid);