mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 05:00:55 +01:00
Update TGS DMAPI
This commit is contained in:
+17
-4
@@ -44,8 +44,17 @@
|
||||
|
||||
//EVENT CODES
|
||||
|
||||
#define TGS_EVENT_PORT_SWAP -2 //before a port change is about to happen, extra parameter is new port
|
||||
#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //before a reboot mode change, extras parameters are the current and new reboot mode enums
|
||||
|
||||
//TODO
|
||||
|
||||
//OTHER ENUMS
|
||||
|
||||
#define TGS_REBOOT_MODE_NORMAL 0
|
||||
#define TGS_REBOOT_MODE_SHUTDOWN 1
|
||||
#define TGS_REBOOT_MODE_RESTART 2
|
||||
|
||||
//REQUIRED HOOKS
|
||||
|
||||
//Call this somewhere in /world/New() that is always run
|
||||
@@ -55,6 +64,8 @@
|
||||
|
||||
//Call this when your initializations are complete and your game is ready to play before any player interactions happen
|
||||
//This may use world.sleep_offline to make this happen so ensure no changes are made to it while this call is running
|
||||
//Most importantly, before this point, note that any static files or directories may be in use by another server. Your code should account for this
|
||||
//This function should not be called before ..() in /world/New()
|
||||
/world/proc/TgsInitializationComplete()
|
||||
return
|
||||
|
||||
@@ -88,20 +99,22 @@
|
||||
/datum/tgs_chat_channel
|
||||
var/id //internal channel representation
|
||||
var/friendly_name //user friendly channel name
|
||||
var/server_name //server name the channel resides on
|
||||
var/provider_name //chat provider for the channel
|
||||
var/connection_name //the name of the configured chat connection
|
||||
var/is_admin_channel //if the server operator has marked this channel for game admins only
|
||||
var/is_private_channel //if this is a private chat channel
|
||||
var/custom_tag //user defined string associated with channel
|
||||
|
||||
//represents a chat user
|
||||
/datum/tgs_chat_user
|
||||
var/id //Internal user representation
|
||||
var/id //Internal user representation, requires channel to be unique
|
||||
var/friendly_name //The user's public name
|
||||
var/mention //The text to use to ping this user in a message
|
||||
var/datum/tgs_chat_channel/channel //The /datum/tgs_chat_channel this user was from
|
||||
|
||||
//user definable callback for handling events
|
||||
/datum/tgs_event_handler/proc/HandleEvent(event_code)
|
||||
//extra parameters may be specified depending on the event
|
||||
/datum/tgs_event_handler/proc/HandleEvent(event_code, ...)
|
||||
set waitfor = FALSE
|
||||
return
|
||||
|
||||
//user definable chat command
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/world/TgsNew(datum/tgs_event_handler/event_handler)
|
||||
var/current_api = TGS_READ_GLOBAL(tgs)
|
||||
if(current_api)
|
||||
TGS_ERROR_LOG("TgsNew(): TGS API datum already set ([current_api])!")
|
||||
return
|
||||
|
||||
var/tgs_version = world.params[TGS_VERSION_PARAMETER]
|
||||
if(!tgs_version)
|
||||
return
|
||||
@@ -6,20 +11,22 @@
|
||||
var/path = SelectTgsApi(tgs_version)
|
||||
if(!path)
|
||||
TGS_ERROR_LOG("Found unsupported API version: [tgs_version]. If this is a valid version please report this, backporting is done on demand.")
|
||||
return
|
||||
|
||||
TGS_INFO_LOG("Activating API for version [tgs_version]")
|
||||
var/datum/tgs_api/new_api = new path
|
||||
|
||||
TGS_WRITE_GLOBAL(tgs, new_api)
|
||||
|
||||
var/result = new_api.OnWorldNew(event_handler ? event_handler : new /datum/tgs_event_handler/tgs_default)
|
||||
if(result && result != TGS_UNIMPLEMENTED)
|
||||
TGS_WRITE_GLOBAL(tgs, new_api)
|
||||
else
|
||||
if(!result || result == TGS_UNIMPLEMENTED)
|
||||
TGS_WRITE_GLOBAL(tgs, null)
|
||||
TGS_ERROR_LOG("Failed to activate API!")
|
||||
|
||||
/world/proc/SelectTgsApi(tgs_version)
|
||||
//remove the old 3.0 header
|
||||
tgs_version = replacetext(tgs_version, "/tg/station 13 Server v", "")
|
||||
|
||||
|
||||
var/list/version_bits = splittext(tgs_version, ".")
|
||||
|
||||
var/super = text2num(version_bits[1])
|
||||
@@ -32,6 +39,10 @@
|
||||
switch(major)
|
||||
if(2)
|
||||
return /datum/tgs_api/v3210
|
||||
if(4)
|
||||
switch(major)
|
||||
if(0)
|
||||
return /datum/tgs_api/v4
|
||||
|
||||
if(super != null && major != null && minor != null && patch != null && tgs_version > TgsMaximumAPIVersion())
|
||||
TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
|
||||
|
||||
@@ -3,7 +3,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
|
||||
/datum/tgs_api
|
||||
|
||||
/datum/tgs_api/latest
|
||||
parent_type = /datum/tgs_api/v3210
|
||||
parent_type = /datum/tgs_api/v4
|
||||
|
||||
TGS_PROTECT_DATUM(/datum/tgs_api)
|
||||
|
||||
|
||||
@@ -4,3 +4,5 @@
|
||||
#include "core\default_event_handler.dm"
|
||||
#include "v3210\api.dm"
|
||||
#include "v3210\commands.dm"
|
||||
#include "v4\api.dm"
|
||||
#include "v4\commands.dm"
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
/datum/tgs_api/v3210/OnWorldNew(datum/tgs_event_handler/event_handler) //don't use event handling in this version
|
||||
. = FALSE
|
||||
|
||||
comms_key = world.params[SERVICE_WORLD_PARAM]
|
||||
instance_name = world.params[SERVICE_INSTANCE_PARAM]
|
||||
if(!instance_name)
|
||||
@@ -187,7 +188,7 @@
|
||||
/datum/tgs_api/v3210/ChatTargetedBroadcast(message, admin_only)
|
||||
ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message]")
|
||||
|
||||
/datum/tgs_api/v3210/ChatPrivateMessage(message, admin_only)
|
||||
/datum/tgs_api/v3210/ChatPrivateMessage(message, datum/tgs_chat_user/user)
|
||||
return TGS_UNIMPLEMENTED
|
||||
|
||||
#undef REBOOT_MODE_NORMAL
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
#define TGS4_PARAM_INFO_JSON "tgs_json"
|
||||
|
||||
#define TGS4_INTEROP_ACCESS_IDENTIFIER "tgs_tok"
|
||||
|
||||
#define TGS4_RESPONSE_SUCCESS "tgs_succ"
|
||||
|
||||
#define TGS4_TOPIC_CHANGE_PORT "tgs_port"
|
||||
#define TGS4_TOPIC_CHANGE_REBOOT_MODE "tgs_rmode"
|
||||
#define TGS4_TOPIC_CHAT_COMMAND "tgs_chat_comm"
|
||||
#define TGS4_TOPIC_EVENT "tgs_event"
|
||||
#define TGS4_TOPIC_INTEROP_RESPONSE "tgs_interop"
|
||||
|
||||
#define TGS4_COMM_NEW_PORT "tgs_new_port"
|
||||
#define TGS4_COMM_VALIDATE "tgs_validate"
|
||||
#define TGS4_COMM_SERVER_PRIMED "tgs_prime"
|
||||
#define TGS4_COMM_WORLD_REBOOT "tgs_reboot"
|
||||
#define TGS4_COMM_END_PROCESS "tgs_kill"
|
||||
#define TGS4_COMM_CHAT "tgs_chat_send"
|
||||
|
||||
#define TGS4_PARAMETER_COMMAND "tgs_com"
|
||||
#define TGS4_PARAMETER_DATA "tgs_data"
|
||||
|
||||
#define TGS4_PORT_CRITFAIL_MESSAGE " Must exit to let watchdog reboot..."
|
||||
|
||||
#define EXPORT_TIMEOUT_DS 200
|
||||
|
||||
/datum/tgs_api/v4
|
||||
var/access_identifier
|
||||
var/instance_name
|
||||
var/json_path
|
||||
var/chat_channels_json_path
|
||||
var/chat_commands_json_path
|
||||
var/server_commands_json_path
|
||||
var/reboot_mode = TGS_REBOOT_MODE_NORMAL
|
||||
|
||||
var/list/intercepted_message_queue
|
||||
|
||||
var/list/custom_commands
|
||||
|
||||
var/list/cached_test_merges
|
||||
var/datum/tgs_revision_information/cached_revision
|
||||
|
||||
var/datum/tgs_event_handler/event_handler
|
||||
|
||||
var/export_lock = FALSE
|
||||
var/list/last_interop_response
|
||||
|
||||
/datum/tgs_api/v4/ApiVersion()
|
||||
return "4.0.0.0"
|
||||
|
||||
/datum/tgs_api/v4/OnWorldNew(datum/tgs_event_handler/event_handler)
|
||||
json_path = world.params[TGS4_PARAM_INFO_JSON]
|
||||
if(!json_path)
|
||||
TGS_ERROR_LOG("Missing [TGS4_PARAM_INFO_JSON] world parameter!")
|
||||
return
|
||||
var/json_file = file2text(json_path)
|
||||
if(!json_file)
|
||||
TGS_ERROR_LOG("Missing specified json file: [json_path]")
|
||||
return
|
||||
var/cached_json = json_decode(json_file)
|
||||
if(!cached_json)
|
||||
TGS_ERROR_LOG("Failed to decode info json: [json_file]")
|
||||
return
|
||||
|
||||
access_identifier = cached_json["accessIdentifier"]
|
||||
instance_name = text2num(cached_json["instanceName"])
|
||||
server_commands_json_path = cached_json["serverCommandsJson"]
|
||||
|
||||
if(cached_json["apiValidateOnly"])
|
||||
TGS_INFO_LOG("Validating API and exiting...")
|
||||
Export(TGS4_COMM_VALIDATE)
|
||||
del(world)
|
||||
|
||||
chat_channels_json_path = cached_json["chatChannelsJson"]
|
||||
chat_commands_json_path = cached_json["chatCommandsJson"]
|
||||
src.event_handler = event_handler
|
||||
instance_name = cached_json["instanceName"]
|
||||
|
||||
ListCustomCommands()
|
||||
|
||||
var/list/revisionData = cached_json["revision"]
|
||||
if(revisionData)
|
||||
cached_revision = new
|
||||
cached_revision.commit = revisionData["commitSha"]
|
||||
cached_revision.origin_commit = revisionData["originCommitSha"]
|
||||
|
||||
cached_test_merges = list()
|
||||
var/list/json = cached_json["testMerges"]
|
||||
for(var/entry in json)
|
||||
var/datum/tgs_revision_information/test_merge/tm = new
|
||||
tm.time_merged = text2num(entry["timeMerged"])
|
||||
|
||||
var/list/revInfo = entry["revision"]
|
||||
if(revInfo)
|
||||
tm.commit = revInfo["commitSha"]
|
||||
tm.origin_commit = revInfo["originCommitSha"]
|
||||
|
||||
tm.title = entry["titleAtMerge"]
|
||||
tm.body = entry["bodyAtMerge"]
|
||||
tm.url = entry["url"]
|
||||
tm.author = entry["author"]
|
||||
tm.number = entry["number"]
|
||||
tm.pull_request_commit = entry["pullRequestRevision"]
|
||||
tm.comment = entry["comment"]
|
||||
|
||||
cached_test_merges += tm
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/tgs_api/v4/OnInitializationComplete()
|
||||
Export(TGS4_COMM_SERVER_PRIMED)
|
||||
|
||||
var/tgs4_secret_sleep_offline_sauce = 24051994
|
||||
var/old_sleep_offline = world.sleep_offline
|
||||
world.sleep_offline = tgs4_secret_sleep_offline_sauce
|
||||
sleep(1)
|
||||
if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it
|
||||
world.sleep_offline = old_sleep_offline
|
||||
|
||||
/datum/tgs_api/v4/OnTopic(T)
|
||||
var/list/params = params2list(T)
|
||||
var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER]
|
||||
if(!their_sCK)
|
||||
return FALSE //continue world/Topic
|
||||
|
||||
if(their_sCK != access_identifier)
|
||||
return "Invalid comms key!";
|
||||
|
||||
var/command = params[TGS4_PARAMETER_COMMAND]
|
||||
if(!command)
|
||||
return "No command!"
|
||||
|
||||
. = TGS4_RESPONSE_SUCCESS
|
||||
|
||||
switch(command)
|
||||
if(TGS4_TOPIC_CHAT_COMMAND)
|
||||
var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA])
|
||||
if(result == null)
|
||||
result = "Error running chat command!"
|
||||
return result
|
||||
if(TGS4_TOPIC_EVENT)
|
||||
intercepted_message_queue = list()
|
||||
var/list/event_notification = json_decode(params[TGS4_PARAMETER_DATA])
|
||||
var/list/event_parameters = event_notification["Parameters"]
|
||||
|
||||
var/list/event_call = list(event_notification["Type"])
|
||||
if(event_parameters)
|
||||
event_call += event_parameters
|
||||
|
||||
event_handler.HandleEvent(arglist(event_call))
|
||||
|
||||
. = json_encode(intercepted_message_queue)
|
||||
intercepted_message_queue = null
|
||||
return
|
||||
if(TGS4_TOPIC_INTEROP_RESPONSE)
|
||||
last_interop_response = json_decode(params[TGS4_PARAMETER_DATA])
|
||||
return
|
||||
if(TGS4_TOPIC_CHANGE_PORT)
|
||||
var/new_port = text2num(params[TGS4_PARAMETER_DATA])
|
||||
if (!(new_port > 0))
|
||||
return "Invalid port: [new_port]"
|
||||
|
||||
//the topic still completes, miraculously
|
||||
//I honestly didn't believe byond could do it
|
||||
event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
|
||||
if(!world.OpenPort(new_port))
|
||||
return "Port change failed!"
|
||||
if(TGS4_TOPIC_CHANGE_REBOOT_MODE)
|
||||
var/new_reboot_mode = text2num(params[TGS4_PARAMETER_DATA])
|
||||
event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
|
||||
reboot_mode = new_reboot_mode
|
||||
return
|
||||
|
||||
return "Unknown command: [command]"
|
||||
|
||||
/datum/tgs_api/v4/proc/Export(command, list/data)
|
||||
if(!data)
|
||||
data = list()
|
||||
data[TGS4_PARAMETER_COMMAND] = command
|
||||
var/json = json_encode(data)
|
||||
|
||||
//we need some port open at this point to facilitate return communication
|
||||
if(!world.port)
|
||||
if(!world.OpenPort(0)) //open any port
|
||||
TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]")
|
||||
del(world)
|
||||
|
||||
//request a new port
|
||||
export_lock = FALSE
|
||||
var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list("current_port" = "[world.port]")) //stringify this on purpose
|
||||
|
||||
if(!new_port_json)
|
||||
TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]")
|
||||
del(world)
|
||||
|
||||
var/new_port = new_port_json["port"]
|
||||
if(!isnum(new_port) || new_port <= 0)
|
||||
TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]")
|
||||
del(world)
|
||||
|
||||
if(new_port != world.port && !world.OpenPort(new_port))
|
||||
TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]")
|
||||
del(world)
|
||||
|
||||
while(export_lock)
|
||||
sleep(1)
|
||||
export_lock = TRUE
|
||||
|
||||
last_interop_response = null
|
||||
fdel(server_commands_json_path)
|
||||
text2file(json, server_commands_json_path)
|
||||
|
||||
for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I)
|
||||
sleep(1)
|
||||
|
||||
if(!last_interop_response)
|
||||
TGS_ERROR_LOG("Failed to get export result for: [json]")
|
||||
else
|
||||
. = last_interop_response
|
||||
|
||||
export_lock = FALSE
|
||||
|
||||
/datum/tgs_api/v4/OnReboot()
|
||||
var/json = Export(TGS4_COMM_WORLD_REBOOT)
|
||||
var/list/result = json_decode(json)
|
||||
if(!result)
|
||||
return
|
||||
|
||||
//okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter
|
||||
|
||||
var/port = json[TGS4_PARAMETER_DATA]
|
||||
if(!isnum(port))
|
||||
return //this is valid, server may just want use to reboot
|
||||
|
||||
if(port == 0)
|
||||
//to byond 0 means any port and "none" means close vOv
|
||||
port = "none"
|
||||
|
||||
if(!world.OpenPort(port))
|
||||
TGS_ERROR_LOG("Unable to set port to [port]!")
|
||||
|
||||
/datum/tgs_api/v4/InstanceName()
|
||||
return instance_name
|
||||
|
||||
/datum/tgs_api/v4/TestMerges()
|
||||
return cached_test_merges
|
||||
|
||||
/datum/tgs_api/v4/EndProcess()
|
||||
Export(TGS4_COMM_END_PROCESS)
|
||||
|
||||
/datum/tgs_api/v4/Revision()
|
||||
return cached_revision
|
||||
|
||||
/datum/tgs_api/v4/ChatBroadcast(message, list/channels)
|
||||
var/list/ids
|
||||
if(length(channels))
|
||||
ids = list()
|
||||
for(var/I in channels)
|
||||
var/datum/tgs_chat_channel/channel = I
|
||||
ids += channel.id
|
||||
message = list("message" = message, "channels" = ids)
|
||||
if(intercepted_message_queue)
|
||||
intercepted_message_queue += list(message)
|
||||
else
|
||||
Export(TGS4_COMM_CHAT, message)
|
||||
|
||||
/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only)
|
||||
message = list("message" = message, "channels" = admin_only ? "admin" : "game")
|
||||
if(intercepted_message_queue)
|
||||
intercepted_message_queue += list(message)
|
||||
else
|
||||
Export(TGS4_COMM_CHAT, message)
|
||||
|
||||
/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user)
|
||||
message = list("message" = message, "user" = list("id" = user.id, "channel" = user.channel.id))
|
||||
if(intercepted_message_queue)
|
||||
intercepted_message_queue += list(message)
|
||||
else
|
||||
Export(TGS4_COMM_CHAT, message)
|
||||
|
||||
/datum/tgs_api/v4/ChatChannelInfo()
|
||||
. = list()
|
||||
//no caching cause tgs may change this
|
||||
var/list/json = json_decode(file2text(chat_channels_json_path))
|
||||
for(var/I in json)
|
||||
. += DecodeChannel(I)
|
||||
|
||||
/datum/tgs_api/v4/proc/DecodeChannel(channel_json)
|
||||
var/datum/tgs_chat_channel/channel = new
|
||||
channel.id = channel_json["id"]
|
||||
channel.friendly_name = channel_json["friendlyName"]
|
||||
channel.connection_name = channel_json["connectionName"]
|
||||
channel.is_admin_channel = channel_json["isAdminChannel"]
|
||||
channel.is_private_channel = channel_json["isPrivateChannel"]
|
||||
channel.custom_tag = channel_json["tag"]
|
||||
return channel
|
||||
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017 Jordan Brown
|
||||
|
||||
Permission is hereby granted, free of charge,
|
||||
to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom
|
||||
the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
@@ -0,0 +1,69 @@
|
||||
/datum/tgs_api/v4/proc/ListCustomCommands()
|
||||
var/results = list()
|
||||
custom_commands = list()
|
||||
for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
|
||||
var/datum/tgs_chat_command/stc = new I
|
||||
var/command_name = stc.name
|
||||
if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
|
||||
TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
|
||||
continue
|
||||
|
||||
if(results[command_name])
|
||||
var/datum/other = custom_commands[command_name]
|
||||
TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
|
||||
continue
|
||||
results += list(list("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only))
|
||||
custom_commands[command_name] = stc
|
||||
|
||||
var/commands_file = chat_commands_json_path
|
||||
if(!commands_file)
|
||||
return
|
||||
text2file(json_encode(results), commands_file)
|
||||
|
||||
/datum/tgs_api/v4/proc/HandleCustomCommand(command_json)
|
||||
var/list/data = json_decode(command_json)
|
||||
var/command = data["command"]
|
||||
var/user = data["user"]
|
||||
var/params = data["params"]
|
||||
|
||||
var/datum/tgs_chat_user/u = new
|
||||
u.id = user["id"]
|
||||
u.friendly_name = user["friendlyName"]
|
||||
u.mention = user["mention"]
|
||||
u.channel = DecodeChannel(user["channel"])
|
||||
|
||||
var/datum/tgs_chat_command/sc = custom_commands[command]
|
||||
if(sc)
|
||||
var/result = sc.Run(u, params)
|
||||
if(result == null)
|
||||
result = ""
|
||||
return result
|
||||
return "Unknown command: [command]!"
|
||||
|
||||
/*
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017 Jordan Brown
|
||||
|
||||
Permission is hereby granted, free of charge,
|
||||
to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom
|
||||
the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
Reference in New Issue
Block a user