From 2262559464d5228915511f6262770dc51a517cd4 Mon Sep 17 00:00:00 2001 From: BotoX Date: Tue, 7 Aug 2018 22:22:27 +0200 Subject: [PATCH] add SMJSONAPI --- SMJSONAPI/configs/Events.cstrike.cfg | 927 ++++++++++++++++++++ SMJSONAPI/example.txt | 127 +++ SMJSONAPI/scripting/API.inc | 49 ++ SMJSONAPI/scripting/GameEvents.inc | 158 ++++ SMJSONAPI/scripting/SMJSONAPI.sp | 472 ++++++++++ SMJSONAPI/scripting/Subscribe.inc | 279 ++++++ SMJSONAPI/scripting/include/AsyncSocket.inc | 1 + SMJSONAPI/scripting/include/smjansson.inc | 1 + 8 files changed, 2014 insertions(+) create mode 100644 SMJSONAPI/configs/Events.cstrike.cfg create mode 100644 SMJSONAPI/example.txt create mode 100644 SMJSONAPI/scripting/API.inc create mode 100644 SMJSONAPI/scripting/GameEvents.inc create mode 100644 SMJSONAPI/scripting/SMJSONAPI.sp create mode 100644 SMJSONAPI/scripting/Subscribe.inc create mode 120000 SMJSONAPI/scripting/include/AsyncSocket.inc create mode 120000 SMJSONAPI/scripting/include/smjansson.inc diff --git a/SMJSONAPI/configs/Events.cstrike.cfg b/SMJSONAPI/configs/Events.cstrike.cfg new file mode 100644 index 00000000..26a0dd93 --- /dev/null +++ b/SMJSONAPI/configs/Events.cstrike.cfg @@ -0,0 +1,927 @@ +//=========== (C) Copyright 1999 Valve, L.L.C. All rights reserved. =========== +// +// The copyright to the contents herein is the property of Valve, L.L.C. +// The contents may be used and/or copied only with the written permission of +// Valve, L.L.C., or in accordance with the terms and conditions stipulated in +// the agreement/contract under which the contents have been supplied. +//============================================================================= + +// No spaces in event names, max length 32 +// All strings are case sensitive +// total game event byte length must be < 1024 +// +// valid data key types are: +// none : value is not networked +// string : a zero terminated string +// bool : unsigned int, 1 bit +// byte : unsigned int, 8 bit +// short : signed int, 16 bit +// long : signed int, 32 bit +// float : float, 32 bit + + +// gameevents + hltvevents + replayevents + engineevents +"allevents" +{ +////////////////////////////////////////////////////////////////////// +// Game events +////////////////////////////////////////////////////////////////////// + + "team_info" // info about team + { + "teamid" "byte" // unique team id + "teamname" "string" // team name eg "Team Blue" + } + + "team_score" // team score changed + { + "teamid" "byte" // team id + "score" "short" // total team score + } + + "teamplay_broadcast_audio" // emits a sound to everyone on a team + { + "team" "byte" // unique team id + "sound" "string" // name of the sound to emit + } + + ////////////////////////////////////////////////////////////////////// + // Player events + ////////////////////////////////////////////////////////////////////// + +// "player_team" // player change his team +// { +// "userid" "short" // user ID on server +// "team" "byte" // team id +// "oldteam" "byte" // old team id +// "disconnect" "bool" // team change because player disconnects +// "autoteam" "bool" // true if the player was auto assigned to the team +// "silent" "bool" // if true wont print the team join messages +// "name" "string" // player's name +// } + + "player_class" // a player changed his class + { + "userid" "short" // user ID on server + "class" "string" // new player class / model + } + +// "player_death" // a game event, name may be 32 charaters long +// { +// "userid" "short" // user ID who died +// "attacker" "short" // user ID who killed +// } + +// "player_hurt" +// { +// "userid" "short" // player index who was hurt +// "attacker" "short" // player index who attacked +// "health" "byte" // remaining health points +// } + + "player_chat" // a public player chat + { + "teamonly" "bool" // true if team only chat + "userid" "short" // chatting player + "text" "string" // chat text + } + + "player_score" // players scores changed + { + "userid" "short" // user ID on server + "kills" "short" // # of kills + "deaths" "short" // # of deaths + "score" "short" // total game score + } + + "player_spawn" // player spawned in game + { + "userid" "short" // user ID on server + } + + "player_shoot" // player shoot his weapon + { + "userid" "short" // user ID on server + "weapon" "byte" // weapon ID + "mode" "byte" // weapon mode + } + + "player_use" + { + "userid" "short" // user ID on server + "entity" "short" // entity used by player + } + + "player_changename" + { + "userid" "short" // user ID on server + "oldname" "string" // players old (current) name + "newname" "string" // players new name + } + + "player_hintmessage" + { + "hintmessage" "string" // localizable string of a hint + } + + "base_player_teleported" + { + "entindex" "short" + } + + ////////////////////////////////////////////////////////////////////// + // Game events + ////////////////////////////////////////////////////////////////////// + + "game_init" // sent when a new game is started + { + } + + "game_newmap" // send when new map is completely loaded + { + "mapname" "string" // map name + } + + "game_start" // a new game starts + { + "roundslimit" "long" // max round + "timelimit" "long" // time limit + "fraglimit" "long" // frag limit + "objective" "string" // round objective + } + + "game_end" // a game ended + { + "winner" "byte" // winner team/user id + } + + "round_start" + { + "timelimit" "long" // round time limit in seconds + "fraglimit" "long" // frag limit in seconds + "objective" "string" // round objective + } + + "round_end" + { + "winner" "byte" // winner team/user i + "reason" "byte" // reson why team won + "message" "string" // end round message + } + + "game_message" // a message send by game logic to everyone + { + "target" "byte" // 0 = console, 1 = HUD + "text" "string" // the message text + } + + "break_breakable" + { + "entindex" "long" + "userid" "short" + "material" "byte" // BREAK_GLASS, BREAK_WOOD, etc + } + + "break_prop" + { + "entindex" "long" + "userid" "short" + } + + "entity_killed" + { + "entindex_killed" "long" + "entindex_attacker" "long" + "entindex_inflictor" "long" + "damagebits" "long" + } + + "bonus_updated" + { + "numadvanced" "short" + "numbronze" "short" + "numsilver" "short" + "numgold" "short" + } + + "achievement_event" + { + "achievement_name" "string" // non-localized name of achievement + "cur_val" "short" // # of steps toward achievement + "max_val" "short" // total # of steps in achievement + } + + // sent whenever an achievement that's tracked on the HUD increases + "achievement_increment" + { + "achievement_id" "long" // ID of achievement that went up + "cur_val" "short" // # of steps toward achievement + "max_val" "short" // total # of steps in achievement + } + + "physgun_pickup" + { + "entindex" "long" // entity picked up + } + + "flare_ignite_npc" + { + "entindex" "long" // entity ignited + } + + "helicopter_grenade_punt_miss" + { + } + + "user_data_downloaded" // fired when achievements/stats are downloaded from Steam or XBox Live + { + } + + "ragdoll_dissolved" + { + "entindex" "long" + } + + "hltv_changed_mode" + { + "oldmode" "short" + "newmode" "short" + "obs_target" "short" + } + "hltv_changed_target" + { + "mode" "short" + "old_target" "short" + "obs_target" "short" + } + + // Client side VoteController talking to HUD + "vote_ended" + { + } + "vote_started" + { + "issue" "string" + "param1" "string" + "team" "byte" + "initiator" "long" // entity id of the player who initiated the vote + } + "vote_changed" + { + "vote_option1" "byte" + "vote_option2" "byte" + "vote_option3" "byte" + "vote_option4" "byte" + "vote_option5" "byte" + "potentialVotes" "byte" + } + "vote_passed" + { + "details" "string" + "param1" "string" + "team" "byte" + } + "vote_failed" + { + "team" "byte" + } + "vote_cast" + { + "vote_option" "byte" // which option the player voted on + "team" "short" + "entityid" "long" // entity id of the voter + } + "vote_options" + { + "count" "byte" // Number of options - up to MAX_VOTE_OPTIONS + "option1" "string" + "option2" "string" + "option3" "string" + "option4" "string" + "option5" "string" + } + + ////////////////////////////////////////////////////////////////////// + // Replay events + ////////////////////////////////////////////////////////////////////// + + "replay_saved" + { + } + + "entered_performance_mode" + { + } + + "browse_replays" + { + } + + "replay_youtube_stats" + { + "views" "long" + "likes" "long" + "favorited" "long" + } + + ////////////////////////////////////////////////////////////////////// + // Economy events + ////////////////////////////////////////////////////////////////////// + + "inventory_updated" + { + } + "cart_updated" + { + } + "store_pricesheet_updated" + { + } + "gc_connected" + { + } + "item_schema_initialized" + { + } + + +////////////////////////////////////////////////////////////////////// +// HLTV specific events +////////////////////////////////////////////////////////////////////// + + "hltv_status" // general HLTV status + { + "clients" "long" // number of HLTV spectators + "slots" "long" // number of HLTV slots + "proxies" "short" // number of HLTV proxies + "master" "string" // disptach master IP:port + } + + "hltv_cameraman" // a spectator/player is a cameraman + { + "index" "short" // camera man entity index + } + + "hltv_rank_camera" // a camera ranking + { + "index" "byte" // fixed camera index + "rank" "float" // ranking, how interesting is this camera view + "target" "short" // best/closest target entity + } + + "hltv_rank_entity" // an entity ranking + { + "index" "short" // entity index + "rank" "float" // ranking, how interesting is this entity to view + "target" "short" // best/closest target entity + } + + "hltv_fixed" // show from fixed view + { + "posx" "long" // camera position in world + "posy" "long" + "posz" "long" + "theta" "short" // camera angles + "phi" "short" + "offset" "short" + "fov" "float" + "target" "short" // follow this entity or 0 + } + + "hltv_chase" // shot of a single entity + { + "target1" "short" // primary traget index + "target2" "short" // secondary traget index or 0 + "distance" "short" // camera distance + "theta" "short" // view angle horizontal + "phi" "short" // view angle vertical + "inertia" "byte" // camera inertia + "ineye" "byte" // diretcor suggests to show ineye + } + + "hltv_message" // a HLTV message send by moderators + { + "text" "string" + } + + "hltv_title" + { + "text" "string" + } + + "hltv_chat" // a HLTV chat msg send by spectators + { + "text" "string" + } + + + ////////////////////////////////////////////////////////////////////// + // replay specific events + ////////////////////////////////////////////////////////////////////// + + "replay_startrecord" // Sent when the server begins recording - only used to display UI + { + } + + "replay_sessioninfo" // Sent when the server begins recording, or when a client first connects - only sent once per recording session + { + "sn" "string" // session name + "di" "byte" // dump interval + "cb" "long" // current block + "st" "long" // session start tick + } + + "replay_endrecord" + { + } + + "replay_replaysavailable" + { + } + + "replay_servererror" + { + "error" "string" + } + + +////////////////////////////////////////////////////////////////////// +// Server events +////////////////////////////////////////////////////////////////////// + + "server_spawn" // send once a server starts + { + "hostname" "string" // public host name + "address" "string" // hostame, IP or DNS name + "ip" "long" + "port" "short" // server port + "game" "string" // game dir + "mapname" "string" // map name + "maxplayers" "long" // max players + "os" "string" // WIN32, LINUX + "dedicated" "bool" // true if dedicated server + "password" "bool" // true if password protected + } + + "server_changelevel_failed" + { + "levelname" "string" // The level name that failed changelevel + } + + "server_shutdown" // server shut down + { + "reason" "string" // reason why server was shut down + } + + "server_cvar" // a server console var has changed + { + "cvarname" "string" // cvar name, eg "mp_roundtime" + "cvarvalue" "string" // new cvar value + } + + "server_message" // a generic server message + { + "text" "string" // the message text + } + + "server_addban" + { + "name" "string" // player name + "userid" "short" // user ID on server + "networkid" "string" // player network (i.e steam) id + "ip" "string" // IP address + "duration" "string" // length of the ban + "by" "string" // banned by... + "kicked" "bool" // whether the player was also kicked + } + + "server_removeban" + { + "networkid" "string" // player network (i.e steam) id + "ip" "string" // IP address + "by" "string" // removed by... + } + + "player_connect" // a new client connected - we should only really have server listeners for this, due to the IP being exposed + { + "name" "string" // player name + "index" "byte" // player slot (entity index-1) + "userid" "short" // user ID on server (unique on server) + "networkid" "string" // player network (i.e steam) id + "address" "string" // ip:port + "bot" "short" // is a bot + } + + "player_connect_client" // a new client connected + { + "name" "string" // player name + "index" "byte" // player slot (entity index-1) + "userid" "short" // user ID on server (unique on server) + "networkid" "string" // player network (i.e steam) id + "bot" "short" // is a bot + } + + "player_info" // a player changed his name + { + "name" "string" // player name + "index" "byte" // player slot (entity index-1) + "userid" "short" // user ID on server (unique on server) + "networkid" "string" // player network (i.e steam) id + "bot" "bool" // true if player is a AI bot + } + + "player_disconnect" // a client was disconnected + { + "userid" "short" // user ID on server + "reason" "string" // "self", "kick", "ban", "cheat", "error" + "name" "string" // player name + "networkid" "string" // player network (i.e steam) id + "bot" "short" // is a bot + } + + "player_activate" + { + "userid" "short" // user ID on server + } + + "player_say" + { + "userid" "short" // user ID on server + "text" "string" // the say text + } + + "client_disconnect" // client side disconnect message + { + "message" "string" // Why are we disconnecting? This could be a localization token or an English-language string + } + + "client_beginconnect" // client tries to connect to server + { + "address" "string" // Name we used to connect to the server + "ip" "long" + "port" "short" // server port + "source" "string" // what caused us to attempt this connection? (blank for general command line, "serverbrowser", "quickplay", etc) + } + + "client_connected" // client has completed the challenge / handshake process and is in SIGNONSTATE_CONNECTED + { + "address" "string" // Name we used to connect to the server + "ip" "long" + "port" "short" // server port + } + + "client_fullconnect" + { + "address" "string" // Name we used to connect to the server + "ip" "long" + "port" "short" // server port + } + + "host_quit" + { + } + + +////////////////////////////////////////////////////////////////////// +// CStrike events +////////////////////////////////////////////////////////////////////// + + "player_death" // a game event, name may be 32 characters long + { + // this extents the original player_death by a new fields + "userid" "short" // user ID who died + "attacker" "short" // user ID who killed + "weapon" "string" // weapon name killer used + "headshot" "bool" // singals a headshot + "dominated" "short" // did killer dominate victim with this kill + "revenge" "short" // did killer get revenge on victim with this kill + } + + "player_hurt" + { + "userid" "short" // player index who was hurt + "attacker" "short" // player index who attacked + "health" "byte" // remaining health points + "armor" "byte" // remaining armor points + "weapon" "string" // weapon name attacker used, if not the world + "dmg_health" "byte" // damage done to health + "dmg_armor" "byte" // damage done to armor + "hitgroup" "byte" // hitgroup that was damaged + } + + "bomb_beginplant" + { + "userid" "short" // player who is planting the bomb + "site" "short" // bombsite index + } + + "bomb_abortplant" + { + "userid" "short" // player who is planting the bomb + "site" "short" // bombsite index + } + + "bomb_planted" + { + "userid" "short" // player who planted the bomb + "site" "short" // bombsite index + "posx" "short" // position x + "posy" "short" // position y + } + + "bomb_defused" + { + "userid" "short" // player who defused the bomb + "site" "short" // bombsite index + } + + "bomb_exploded" + { + "userid" "short" // player who planted the bomb + "site" "short" // bombsite index + } + + "bomb_dropped" + { + "userid" "short" // player who dropped the bomb + } + + "bomb_pickup" + { + "userid" "short" // player who picked up the bomb + } + + "bomb_begindefuse" + { + "userid" "short" // player who is defusing + "haskit" "bool" + } + + "bomb_abortdefuse" + { + "userid" "short" // player who was defusing + } + + "hostage_follows" + { + "userid" "short" // player who touched the hostage + "hostage" "short" // hostage entity index + } + + "hostage_hurt" + { + "userid" "short" // player who hurt the hostage + "hostage" "short" // hostage entity index + } + + "hostage_killed" + { + "userid" "short" // player who killed the hostage + "hostage" "short" // hostage entity index + } + + "hostage_rescued" + { + "userid" "short" // player who rescued the hostage + "hostage" "short" // hostage entity index + "site" "short" // rescue site index + } + + "hostage_stops_following" + { + "userid" "short" // player who rescued the hostage + "hostage" "short" // hostage entity index + } + + "hostage_rescued_all" + { + } + + "hostage_call_for_help" + { + "hostage" "short" // hostage entity index + } + + "vip_escaped" + { + "userid" "short" // player who was the VIP + } + + "vip_killed" + { + "userid" "short" // player who was the VIP + "attacker" "short" // user ID who killed the VIP + } + + "player_radio" + { + "userid" "short" + "slot" "short" + } + + "bomb_beep" + { + "entindex" "long" // c4 entity + } + + "weapon_fire" + { + "userid" "short" + "weapon" "string" // weapon name used + } + + "weapon_fire_on_empty" + { + "userid" "short" + "weapon" "string" // weapon name used + } + + "weapon_reload" + { + "userid" "short" + } + + "weapon_zoom" + { + "userid" "short" + } + + "item_pickup" + { + "userid" "short" + "item" "string" // either a weapon such as 'tmp' or 'hegrenade', or an item such as 'nvgs' + } + + "grenade_bounce" + { + "userid" "short" + } + + "hegrenade_detonate" + { + "userid" "short" + "x" "float" + "y" "float" + "z" "float" + } + + "flashbang_detonate" + { + "userid" "short" + "x" "float" + "y" "float" + "z" "float" + } + + "smokegrenade_detonate" + { + "userid" "short" + "x" "float" + "y" "float" + "z" "float" + } + + "bullet_impact" + { + "userid" "short" + "x" "float" + "y" "float" + "z" "float" + } + + "player_footstep" + { + "userid" "short" + } + + "player_jump" + { + "userid" "short" + } + + "player_blind" + { + "userid" "short" + } + + "player_falldamage" + { + "userid" "short" + "damage" "float" + } + + "door_moving" + { + "entindex" "long" + "userid" "short" + } + + "round_freeze_end" + { + } + + "nav_blocked" + { + "area" "long" + "blocked" "bool" + } + + "nav_generate" + { + } + + "player_stats_updated" + { + "forceupload" "bool" + } + + "spec_target_updated" + { + } + + "cs_win_panel_round" + { + "show_timer_defend" "bool" + "show_timer_attack" "bool" + "timer_time" "short" + + "final_event" "byte" //define in cs_gamerules.h + + "funfact_token" "string" + "funfact_player" "short" + "funfact_data1" "long" + "funfact_data2" "long" + "funfact_data3" "long" + } + + "cs_win_panel_match" + { + "t_score" "short" + "ct_score" "short" + "t_kd" "float" + "ct_kd" "float" + "t_objectives_done" "short" + "ct_objectives_done" "short" + "t_money_earned" "long" + "ct_money_earned" "long" + } + + "show_freezepanel" + { + "killer" "short" // entindex of the killer entity + } + + "hide_freezepanel" + { + } + + "freezecam_started" + { + } + + "player_avenged_teammate" + { + "avenger_id" "short" + "avenged_player_id" "short" + } + + "achievement_earned" + { + "player" "byte" // entindex of the player + "achievement" "short" // achievement ID + } + + "achievement_earned_local" + { + "achievement" "short" // achievement ID + } + + "match_end_conditions" + { + "frags" "long" + "max_rounds" "long" + "win_rounds" "long" + "time" "long" + } + + "round_mvp" + { + "userid" "short" + "reason" "short" + } + + "player_decal" + { + "userid" "short" + } + + "teamplay_round_start" // round restart + { + "full_reset" "bool" // is this a full reset of the map + } + + "christmas_gift_grab" + { + "userid" "short" + } +} diff --git a/SMJSONAPI/example.txt b/SMJSONAPI/example.txt new file mode 100644 index 00000000..b226ec39 --- /dev/null +++ b/SMJSONAPI/example.txt @@ -0,0 +1,127 @@ +request: +{ + "method": "subscribe", + "module": "gameevents", + "events": [ + "player_connect", + "player_disconnect", + "player_say", + "invalid" + ] +} + +response: +{ + "method": "subscribe", + "module": "gameevents", + "error": 0, + "events": [ + 0, + 1, + 2, + -3 + ] +} + +----- + +events: +{ + "method": "publish", + "module": "gameevents", + "event": { + "name": "player_connect", + "data": { + "name": "BotoX", + "index": 0, + "userid": 4, + "networkid": "[U:1:51174697]", + "address": "192.168.1.4:27006", + "bot": 0 + } + } +} + +{ + "method": "publish", + "module": "gameevents", + "event": { + "name": "player_say", + "data": { + "text": "hello", + "userid": 4 + } + } +} + +{ + "method": "publish", + "module": "gameevents", + "event": { + "name": "player_disconnect", + "data": { + "userid": 4, + "reason": "Disconnect by user.", + "name": "BotoX", + "networkid": "[U:1:51174697]", + "bot": 0 + } + } +} + +----- + +request: +{ + "method": "unsubscribe", + "module": "gameevents", + "events": [ + "player_connect", + "player_disconnect", + "player_say", + "invalid" + ] +} + +response: +{ + "method": "unsubscribe", + "module": "gameevents", + "error": 0, + "events": [ + 0, + 0, + 0, + 0 + ] +} + +----- + +request: +{ + "method": "function", + "function": "KickClient", + "parameters": [ + 1, + "%d hello %s", + 1337, + "cat" + ] +} + +Other examples: + +{ "method": "function", "function": "PrintToServer", "parameters": ["1: %s, 2: %d, 3: %s, 4: %d", "1test1", [2222], "3test3", [4444]] } + +{ "method": "function", "function": "TeleportEntity", "parameters": [1, [206.472443, 2418.516357, -62.058395], [26.994024, -14.119852, 0.000000], [["NULL_VECTOR"]] ] } + +{ "method": "function", "function": "GetClientName", "parameters": [ 1, "", 1024 ] } + +{ "method": "function", "function": "GetClientAbsOrigin", "parameters": [ 1, [0.0,0,0] ] } + +{ "method": "function", "function": "ServerCommandEx", "parameters": [ "", 1024, "echo hello mates %d %f %s", [123], [123.456], "test" ] } + +{ "method": "subscribe", "module": "gameevents", "events": [ "player_say" ] } + +{ "method": "function", "function": "GetUserFlagBits", "args": [ 9 ] } diff --git a/SMJSONAPI/scripting/API.inc b/SMJSONAPI/scripting/API.inc new file mode 100644 index 00000000..d8ffcdbf --- /dev/null +++ b/SMJSONAPI/scripting/API.inc @@ -0,0 +1,49 @@ +public void API_PrintToChatAll(const char[] format, any ...) +{ + char buffer[254]; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), format, 2); + PrintToChat(i, "%s", buffer); + } + } +} + +public void API_PrintCenterTextAll(const char[] format, any ...) +{ + char buffer[254]; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), format, 2); + PrintCenterText(i, "%s", buffer); + } + } +} + +public void API_PrintHintTextToAll(const char[] format, any ...) +{ + char buffer[254]; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), format, 2); + PrintHintText(i, "%s", buffer); + } + } +} + +public int API_GetMaxClients() +{ + return MaxClients; +} diff --git a/SMJSONAPI/scripting/GameEvents.inc b/SMJSONAPI/scripting/GameEvents.inc new file mode 100644 index 00000000..57673ee3 --- /dev/null +++ b/SMJSONAPI/scripting/GameEvents.inc @@ -0,0 +1,158 @@ +static KeyValues g_Config; + +bool GameEvents_Init() +{ + char sGame[32]; + GetGameFolderName(sGame, sizeof(sGame)); + + char sConfigFile[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sConfigFile, sizeof(sConfigFile), "configs/Events.%s.cfg", sGame); + if(!FileExists(sConfigFile)) + { + SetFailState("Could not find config: \"%s\"", sConfigFile); + } + + g_Config = new KeyValues("allevents"); + if(!g_Config.ImportFromFile(sConfigFile)) + { + delete g_Config; + SetFailState("ImportFromFile() failed!"); + } + + return true; +} + +static void GameEvents_OnHook(Event event, const char[] name, bool dontBroadcast) +{ + static char sEventName[32]; + event.GetName(sEventName, sizeof(sEventName)); + + g_Config.JumpToKey(sEventName); + + JSONObject jEvent = new JSONObject(); + jEvent.SetString("name", sEventName); + + JSONObject jEventData = new JSONObject(); + + if(g_Config.GotoFirstSubKey(false)) + { + do + { + static char sKey[32]; + static char sType[8]; + + g_Config.GetSectionName(sKey, sizeof(sKey)); + g_Config.GetString(NULL_STRING, sType, sizeof(sType)); + + if(StrEqual(sKey, "_hooked")) + continue; + + if(StrEqual(sType, "short") || StrEqual(sType, "long") || StrEqual(sType, "byte")) + { + int iValue = event.GetInt(sKey); + jEventData.SetInt(sKey, iValue); + } + else if(StrEqual(sType, "float")) + { + float fValue = event.GetFloat(sKey); + jEventData.SetFloat(sKey, fValue); + } + else if(StrEqual(sType, "bool")) + { + bool bValue = event.GetBool(sKey); + jEventData.SetBool(sKey, bValue); + } + else if(StrEqual(sType, "string")) + { + static char sValue[1024]; + event.GetString(sKey, sValue, sizeof(sValue)); + jEventData.SetString(sKey, sValue); + } + } while(g_Config.GotoNextKey(false)); + g_Config.Rewind(); + } + + jEvent.Set("data", jEventData); + + Subscribe_GameEvents_Publish(sEventName, jEvent); +} + +int GameEvents_Hook(const char[] sEventName) +{ + if(!g_Config.JumpToKey(sEventName)) + return -1; + + int Hooked = g_Config.GetNum("_hooked", 0); + if(Hooked) + { + g_Config.SetNum("_hooked", Hooked + 1); + return Hooked + 1; + } + + Hooked = HookEventEx(sEventName, GameEvents_OnHook, EventHookMode_Post); + if(Hooked) + g_Config.SetNum("_hooked", 1); + + g_Config.Rewind(); + return Hooked ? 1 : -1; +} + +int GameEvents_Unhook(const char[] sEventName) +{ + if(!g_Config.JumpToKey(sEventName)) + return -1; + + int Hooked = g_Config.GetNum("_hooked", 0); + if(!Hooked) + return 0; + + if(Hooked == 1) + UnhookEvent(sEventName, GameEvents_OnHook, EventHookMode_Post); + + g_Config.SetNum("_hooked", Hooked - 1); + + g_Config.Rewind(); + return Hooked - 1; +} + +stock void GameEvents_HookAll() +{ + g_Config.GotoFirstSubKey(false); + + do + { + static char sKey[32]; + g_Config.GetSectionName(sKey, sizeof(sKey)); + + int Hooked = g_Config.GetNum("_hooked", 0); + if(!Hooked) + { + if(HookEventEx(sKey, GameEvents_OnHook, EventHookMode_Post)) + g_Config.SetNum("_hooked", 1); + } + else + g_Config.SetNum("_hooked", Hooked + 1); + } while(g_Config.GotoNextKey(true)); + + g_Config.Rewind(); +} + +stock void GameEvents_UnhookAll() +{ + g_Config.GotoFirstSubKey(false); + + do + { + static char sKey[32]; + g_Config.GetSectionName(sKey, sizeof(sKey)); + + int Hooked = g_Config.GetNum("_hooked", 0); + if(Hooked) + { + UnhookEvent(sKey, GameEvents_OnHook, EventHookMode_Post); + g_Config.SetNum("_hooked", 0); + } + } while(g_Config.GotoNextKey(true)); + + g_Config.Rewind(); +} diff --git a/SMJSONAPI/scripting/SMJSONAPI.sp b/SMJSONAPI/scripting/SMJSONAPI.sp new file mode 100644 index 00000000..f9f6fd7b --- /dev/null +++ b/SMJSONAPI/scripting/SMJSONAPI.sp @@ -0,0 +1,472 @@ +#pragma semicolon 1 +#pragma newdecls required +#pragma dynamic 65535 + +#include +#include +#include + +#define MAX_CLIENTS 16 + +#include "API.inc" +#include "Subscribe.inc" + +static AsyncSocket g_ServerSocket; + +static AsyncSocket g_Client_Socket[MAX_CLIENTS] = { null, ... }; +static int g_Client_Subscriber[MAX_CLIENTS] = { -1, ... }; + +ConVar g_ListenAddr; +ConVar g_ListenPort; + +public Plugin myinfo = +{ + name = "SM JSON API", + author = "SourceMod TCP JSON API", + description = "", + version = "1.0", + url = "" +} + +public void OnPluginStart() +{ + Subscribe_OnPluginStart(); + + g_ListenAddr = CreateConVar("sm_jsonapi_addr", "127.0.0.1", "SM JSON API listen ip address", FCVAR_PROTECTED); + g_ListenPort = CreateConVar("sm_jsonapi_port", "27021", "SM JSON API listen ip address", FCVAR_PROTECTED, true, 1025.0, true, 65535.0); + + AutoExecConfig(true, "plugin.SMJSONAPI"); +} + +public void OnConfigsExecuted() +{ + if(g_ServerSocket) + return; + + g_ServerSocket = new AsyncSocket(); + + g_ServerSocket.SetConnectCallback(OnAsyncConnect); + g_ServerSocket.SetErrorCallback(OnAsyncServerError); + + char sAddr[32]; + g_ListenAddr.GetString(sAddr, sizeof(sAddr)); + int Port = g_ListenPort.IntValue; + + g_ServerSocket.Listen(sAddr, Port); + LogMessage("Listening on %s:%d", sAddr, Port); +} + +static void OnAsyncConnect(AsyncSocket socket) +{ + int Client = GetFreeClientIndex(); + LogMessage("OnAsyncConnect(Client=%d)", Client); + + if(Client == -1) + { + delete socket; + return; + } + + g_Client_Socket[Client] = socket; + socket.SetErrorCallback(OnAsyncClientError); + socket.SetDataCallback(OnAsyncData); +} + +static void OnAsyncServerError(AsyncSocket socket, int error, const char[] errorName) +{ + SetFailState("OnAsyncServerError(): %d, \"%s\"", error, errorName); +} + +static void OnAsyncClientError(AsyncSocket socket, int error, const char[] errorName) +{ + int Client = ClientFromSocket(socket); + LogMessage("OnAsyncClientError(Client=%d): %d, \"%s\"", Client, error, errorName); + + if(Client == -1) + { + delete socket; + return; + } + + if(g_Client_Subscriber[Client] != -1) + { + Subscriber_Destroy(g_Client_Subscriber[Client]); + g_Client_Subscriber[Client] = -1; + } + + g_Client_Socket[Client] = null; + delete socket; +} + +static void OnAsyncData(AsyncSocket socket, const char[] data, const int size) +{ + int Client = ClientFromSocket(socket); + + int iLine; + int iColumn; + static char sError[256]; + JSONValue jRequest = json_load_ex(data, sError, sizeof(sError), iLine, iColumn); + + if(jRequest == null) + { + JSONObject jResponse = new JSONObject(); + + JSONObject jError = new JSONObject(); + jError.SetString("type", "json"); + jError.SetString("error", sError); + jError.SetInt("line", iLine); + jError.SetInt("column", iColumn); + + jResponse.Set("error", jError); + + jResponse.ToString(sError, sizeof(sError)); + socket.WriteNull(sError); + + delete jResponse; + return; + } + + if(jRequest.IsObject) + { + //view_as(jRequest).DumpToServer(); + JSONObject jResponse = new JSONObject(); + // Negative values and objects indicate errors + // 0 and positive integers indicate success + jResponse.SetInt("error", 0); + + HandleRequest(Client, view_as(jRequest), jResponse); + + static char sResponse[4096]; + jResponse.ToString(sResponse, sizeof(sResponse)); + socket.WriteNull(sResponse); + delete jResponse; + } + + delete jRequest; +} + +static int HandleRequest(int Client, JSONObject jRequest, JSONObject jResponse) +{ + static char sMethod[32]; + if(jRequest.GetString("method", sMethod, sizeof(sMethod)) < 0) + { + JSONObject jError = new JSONObject(); + jError.SetString("error", "Request has no 'method' string-value."); + jResponse.Set("error", jError); + return -1; + } + jResponse.SetString("method", sMethod); + + if(StrEqual(sMethod, "subscribe") || StrEqual(sMethod, "unsubscribe") || StrEqual(sMethod, "replay")) + { + int Subscriber = g_Client_Subscriber[Client]; + if(Subscriber == -1) + { + Subscriber = Subscriber_Create(Client); + if(Subscriber < 0) + { + JSONObject jError = new JSONObject(); + jError.SetString("type", "subscribe"); + jError.SetString("error", "Could not allocate a subscriber."); + jError.SetInt("code", Subscriber); + jResponse.Set("error", jError); + return -1; + } + g_Client_Subscriber[Client] = Subscriber; + } + + return Subscriber_HandleRequest(Subscriber, jRequest, jResponse); + } + else if(StrEqual(sMethod, "function")) + { + static char sFunction[64]; + if(jRequest.GetString("function", sFunction, sizeof(sFunction)) < 0) + { + JSONObject jError = new JSONObject(); + jError.SetString("error", "Request has no 'function' string-value."); + jResponse.Set("error", jError); + return -1; + } + jResponse.SetString("function", sFunction); + + static char sAPIFunction[64]; + Format(sAPIFunction, sizeof(sAPIFunction), "API_%s", sFunction); + + Function Fun = GetFunctionByName(INVALID_HANDLE, sAPIFunction); + if(Fun == INVALID_FUNCTION) + { + int Res = Call_StartNative(sFunction); + if(!Res) + { + JSONObject jError = new JSONObject(); + jError.SetString("error", "Invalid function specified."); + jError.SetInt("code", Res); + jResponse.Set("error", jError); + return -1; + } + } + else + Call_StartFunction(INVALID_HANDLE, Fun); + + JSONArray jArgsArray = view_as(jRequest.Get("args")); + if(jArgsArray == null || !jArgsArray.IsArray) + { + delete jArgsArray; + Call_Cancel(); + JSONObject jError = new JSONObject(); + jError.SetString("error", "Request has no 'args' array-value."); + jResponse.Set("error", jError); + return -1; + } + + int aiValues[32]; + int iValues = 0; + + float afValues[32]; + int fValues = 0; + + static char asValues[16][1024]; + int sValues = 0; + + for(int i = 0; i < jArgsArray.Length; i++) + { + bool Fail = false; + JSONValue jValue; + jValue = jArgsArray.Get(i); + + char sType1[32]; + jValue.TypeToString(sType1, sizeof(sType1)); + + if(jValue.IsArray) + { + JSONArray jValueArray = view_as(jValue); + JSONValue jArrayValue = jValueArray.Get(0); + + char sType2[32]; + jArrayValue.TypeToString(sType2, sizeof(sType2)); + + if(jArrayValue.IsInteger || jArrayValue.IsBoolean) + { + int iValues_ = iValues; + for(int j = 0; j < jValueArray.Length; j++) + { + JSONInteger jInteger = view_as(jValueArray.Get(j)); + aiValues[iValues_++] = jInteger.Value; + delete jInteger; + } + + Call_PushArrayEx(aiValues[iValues], jValueArray.Length, SM_PARAM_COPYBACK); + iValues += jValueArray.Length; + } + else if(jArrayValue.IsFloat) + { + int fValues_ = fValues; + for(int j = 0; j < jValueArray.Length; j++) + { + JSONFloat jFloat = view_as(jValueArray.Get(j)); + afValues[fValues_++] = jFloat.Value; + delete jFloat; + } + + Call_PushArrayEx(afValues[fValues], jValueArray.Length, SM_PARAM_COPYBACK); + fValues += jValueArray.Length; + } + /*else if(jArrayValue.IsString) + { + int sValues_ = sValues; + for(int j = 0; j < jValueArray.Length; j++) + { + JSONString jString = view_as(jValueArray.Get(j)); + jString.GetString(asValues[sValues_++], sizeof(asValues[])); + delete jString; + } + + Call_PushArrayEx(view_as(asValues[sValues]), jValueArray.Length, SM_PARAM_COPYBACK); + sValues += jValueArray.Length; + }*/ + else if(jArrayValue.IsArray) // Special + { + static char sSpecial[32]; + view_as(jArrayValue).GetString(0, sSpecial, sizeof(sSpecial)); + + if(StrEqual(sSpecial, "NULL_VECTOR")) + Call_PushArrayEx(NULL_VECTOR, 3, 0); + else if(StrEqual(sSpecial, "NULL_STRING")) + Call_PushString(NULL_STRING); + else + Fail = true; + } + else + Fail = true; + + if(Fail) + { + Call_Cancel(); + delete jArrayValue; + delete jValue; + delete jArgsArray; + + static char sType[16]; + jValue.TypeToString(sType, sizeof(sType)); + + char sError[128]; + FormatEx(sError, sizeof(sError), "Unsupported parameter in list %d of type '%s'", i, sType); + + JSONObject jError = new JSONObject(); + jError.SetString("error", sError); + jResponse.Set("error", jError); + return -1; + } + delete jArrayValue; + } + else if(jValue.IsInteger || jValue.IsBoolean) + { + aiValues[iValues] = view_as(jValue).Value; + Call_PushCell(aiValues[iValues++]); + } + else if(jValue.IsFloat) + { + afValues[fValues] = view_as(jValue).Value; + Call_PushFloat(afValues[fValues++]); + } + else if(jValue.IsString) + { + // Broken: view_as(jValue).GetString(asValues[sValues], sizeof(asValues[])); + JSONString jString = view_as(jValue); + jString.GetString(asValues[sValues], sizeof(asValues[])); + Call_PushStringEx(asValues[sValues++], sizeof(asValues[]), SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); + } + else + { + Call_Cancel(); + delete jValue; + delete jArgsArray; + + static char sType[16]; + jValue.TypeToString(sType, sizeof(sType)); + + char sError[128]; + FormatEx(sError, sizeof(sError), "Unsupported parameter %d of type '%s'", i, sType); + + JSONObject jError = new JSONObject(); + jError.SetString("error", sError); + jResponse.Set("error", jError); + return -1; + } + delete jValue; + } + + int Result; + static char sException[1024]; + int Error = Call_FinishEx(Result, sException, sizeof(sException)); + + if(Error != SP_ERROR_NONE) + { + delete jArgsArray; + JSONObject jError = new JSONObject(); + jError.SetInt("error", Error); + jError.SetString("exception", sException); + jResponse.Set("error", jError); + return -1; + } + + jResponse.SetInt("result", Result); + + JSONArray jArgsResponse = new JSONArray(); + iValues = 0; + fValues = 0; + sValues = 0; + + for(int i = 0; i < jArgsArray.Length; i++) + { + JSONValue jValue; + jValue = jArgsArray.Get(i); + + if(jValue.IsArray) + { + JSONArray jArrayResponse = new JSONArray(); + JSONArray jValueArray = view_as(jValue); + JSONValue jArrayValue = jValueArray.Get(0); + + if(jArrayValue.IsInteger || jArrayValue.IsBoolean) + { + for(int j = 0; j < jValueArray.Length; j++) + jArrayResponse.AppendInt(aiValues[iValues++]); + } + else if(jArrayValue.IsFloat) + { + for(int j = 0; j < jValueArray.Length; j++) + jArrayResponse.AppendFloat(afValues[fValues++]); + } + else if(jArrayValue.IsString) + { + for(int j = 0; j < jValueArray.Length; j++) + jArrayResponse.AppendString(asValues[sValues++]); + } + else if(jArrayValue.IsArray) // Special + { + static char sSpecial[32]; + view_as(jArrayValue).GetString(0, sSpecial, sizeof(sSpecial)); + jArrayResponse.AppendString(sSpecial); + } + delete jArrayValue; + jArgsResponse.Append(jArrayResponse); + } + else if(jValue.IsInteger || jValue.IsBoolean) + { + jArgsResponse.AppendInt(aiValues[iValues++]); + } + else if(jValue.IsFloat) + { + jArgsResponse.AppendFloat(afValues[fValues++]); + } + else if(jValue.IsString) + { + jArgsResponse.AppendString(asValues[sValues++]); + } + delete jValue; + } + + jResponse.Set("args", jArgsResponse); + + delete jArgsArray; + return 0; + } + + JSONObject jError = new JSONObject(); + jError.SetString("error", "No handler found for requested method."); + jResponse.Set("error", jError); + return -1; +} + +int PublishEvent(int Client, JSONObject Object) +{ + if(Client < 0 || Client > MAX_CLIENTS || g_Client_Socket[Client] == null) + return -1; + + static char sEvent[4096]; + Object.ToString(sEvent, sizeof(sEvent)); + g_Client_Socket[Client].WriteNull(sEvent); + + return 0; +} + +static int GetFreeClientIndex() +{ + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(!g_Client_Socket[i]) + return i; + } + return -1; +} + +static int ClientFromSocket(AsyncSocket socket) +{ + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(g_Client_Socket[i] == socket) + return i; + } + return -1; +} diff --git a/SMJSONAPI/scripting/Subscribe.inc b/SMJSONAPI/scripting/Subscribe.inc new file mode 100644 index 00000000..0ee6b4f3 --- /dev/null +++ b/SMJSONAPI/scripting/Subscribe.inc @@ -0,0 +1,279 @@ +#include "GameEvents.inc" + +#define MAX_SUBSCRIBERS MAX_CLIENTS + +enum +{ + eSubscribeError_OutOfRange = -1, + eSubscribeError_Inactive = -2, + eSubscribeError_GameEvent = -3, +} + +static bool g_Subscriber_Active[MAX_SUBSCRIBERS] = { false, ... }; +static int g_Subscriber_Client[MAX_SUBSCRIBERS] = { -1, ... }; + +static ArrayList g_Subscriber_GameEvents[MAX_SUBSCRIBERS]; + +bool Subscribe_OnPluginStart() +{ + GameEvents_Init(); +} + +int Subscriber_Create(int Client) +{ + int Index = Client; + if(Index < 0 || Index >= MAX_SUBSCRIBERS || g_Subscriber_Active[Index]) + { + Index = GetFreeSubscriberIndex(); + if(Index == eSubscribeError_OutOfRange) + return eSubscribeError_OutOfRange; + } + + g_Subscriber_Active[Index] = true; + g_Subscriber_Client[Index] = Client; + + g_Subscriber_GameEvents[Index] = new ArrayList(ByteCountToCells(32)); + + return Index; +} + +int Subscriber_Destroy(int Index) +{ + if(Index < 0 || Index >= MAX_SUBSCRIBERS) + return eSubscribeError_OutOfRange; + + if(!g_Subscriber_Active[Index]) + return eSubscribeError_Inactive; + + for(int i = 0; i < g_Subscriber_GameEvents[Index].Length; i++) + { + static char sEventName[32]; + g_Subscriber_GameEvents[Index].GetString(i, sEventName, sizeof(sEventName)); + GameEvents_Unhook(sEventName); + } + + delete g_Subscriber_GameEvents[Index]; + + g_Subscriber_Active[Index] = false; + g_Subscriber_Client[Index] = -1; + return 0; +} + +int Subscriber_HandleRequest(int Index, JSONObject Request, JSONObject Response) +{ + static char sMethod[32]; + Request.GetString("method", sMethod, sizeof(sMethod)); + + static char sModule[32]; + if(Request.GetString("module", sModule, sizeof(sModule)) < 0) + { + JSONObject jError = new JSONObject(); + jError.SetString("error", "Request has no 'module' string-value."); + Response.Set("error", jError); + return -1; + } + Response.SetString("module", sModule); + + if(StrEqual(sModule, "gameevents")) + { + JSONArray EventArray = view_as(Request.Get("events")); + if(EventArray == null || !EventArray.IsArray) + { + delete EventArray; + JSONObject jError = new JSONObject(); + jError.SetString("error", "Request has no 'events' array-value."); + Response.Set("error", jError); + return -1; + } + + int Method = 0; + if(StrEqual(sMethod, "subscribe")) + Method = 1; + else if(StrEqual(sMethod, "unsubscribe")) + Method = 2; + else if(StrEqual(sMethod, "replay")) + Method = 3; + + JSONArray EventArrayResponse = new JSONArray(); + + for(int i = 0; i < EventArray.Length; i++) + { + static char sEventName[32]; + if(EventArray.GetString(i, sEventName, sizeof(sEventName)) < 0) + { + JSONObject jError = new JSONObject(); + jError.SetString("error", "not a string-value"); + EventArrayResponse.Append(jError); + continue; + } + + int Res; + if(Method == 1) + Res = Subscribe_GameEvents_Subscribe(Index, sEventName); + else if(Method == 2) + Res = Subscribe_GameEvents_Unsubscribe(Index, sEventName); + else if(Method == 3) + Res = Subscribe_GameEvents_Replay(Index, sEventName); + + EventArrayResponse.AppendInt(Res); + } + delete EventArray; + + Response.Set("events", EventArrayResponse); + return 0; + } + + JSONObject jError = new JSONObject(); + jError.SetString("error", "No handler found for requested module."); + Response.Set("error", jError); + return -1; +} + +static int GetFreeSubscriberIndex() +{ + for(int i = 0; i < MAX_SUBSCRIBERS; i++) + { + if(!g_Subscriber_Active[i]) + return i; + } + return eSubscribeError_OutOfRange; +} + +/* GameEvents */ +static int Subscribe_GameEvents_Subscribe(int Index, const char[] sEventName) +{ + if(Index < 0 || Index >= MAX_SUBSCRIBERS) + return eSubscribeError_OutOfRange; + + if(!g_Subscriber_Active[Index]) + return eSubscribeError_Inactive; + + int Find = g_Subscriber_GameEvents[Index].FindString(sEventName); + if(Find != -1) + return Find; + + if(GameEvents_Hook(sEventName) < 0) + return eSubscribeError_GameEvent; + + return g_Subscriber_GameEvents[Index].PushString(sEventName); +} + +static int Subscribe_GameEvents_Unsubscribe(int Index, const char[] sEventName) +{ + if(Index < 0 || Index >= MAX_SUBSCRIBERS) + return eSubscribeError_OutOfRange; + + if(!g_Subscriber_Active[Index]) + return eSubscribeError_Inactive; + + int Find = g_Subscriber_GameEvents[Index].FindString(sEventName); + if(Find == -1) + return 0; + + if(GameEvents_Unhook(sEventName) < 0) + return eSubscribeError_GameEvent; + + g_Subscriber_GameEvents[Index].Erase(Find); + return Find; +} + +static int Subscribe_GameEvents_Replay(int Index, const char[] sEventName) +{ + if(Index < 0 || Index >= MAX_SUBSCRIBERS) + return eSubscribeError_OutOfRange; + + if(!g_Subscriber_Active[Index]) + return eSubscribeError_Inactive; + + if(StrEqual(sEventName, "player_connect")) + { + for(int client = 1; client <= MaxClients; client++) + { + if(!IsClientConnected(client)) + continue; + + char sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + char sSteamID[32]; + GetClientAuthId(client, AuthId_Engine, sSteamID, sizeof(sSteamID), false); + + char sAddress[32]; + GetClientIP(client, sAddress, sizeof(sAddress), false); + + JSONObject jEventData = new JSONObject(); + jEventData.SetString("name", sName); + char sBuf[MAX_NAME_LENGTH]; + jEventData.GetString("name", sBuf, sizeof(sBuf)); + jEventData.SetInt("index", client - 1); + jEventData.SetInt("userid", GetClientUserId(client)); + jEventData.SetString("networkid", sSteamID); + jEventData.SetString("address", sAddress); + jEventData.SetBool("bot", IsFakeClient(client)); + + Subscribe_GameEvents_FakePublish(g_Subscriber_Client[Index], sEventName, jEventData); + } + } + else if(StrEqual(sEventName, "player_activate")) + { + for(int client = 1; client <= MaxClients; client++) + { + if(!IsClientInGame(client)) + continue; + + JSONObject jEventData = new JSONObject(); + jEventData.SetInt("userid", GetClientUserId(client)); + + Subscribe_GameEvents_FakePublish(g_Subscriber_Client[Index], sEventName, jEventData); + } + } + else + return eSubscribeError_GameEvent; + + return 0; +} + +static int Subscribe_GameEvents_FakePublish(int Index, const char[] sEventName, JSONObject jEventData) +{ + if(Index < 0 || Index >= MAX_SUBSCRIBERS) + return eSubscribeError_OutOfRange; + + if(!g_Subscriber_Active[Index]) + return eSubscribeError_Inactive; + + JSONObject jEvent = new JSONObject(); + jEvent.SetString("name", sEventName); + jEvent.Set("data", jEventData); + + JSONObject jPublish = new JSONObject(); + jPublish.SetString("method", "publish"); + jPublish.SetString("module", "gameevents"); + jPublish.Set("event", jEvent); + + PublishEvent(g_Subscriber_Client[Index], jPublish); + delete jPublish; + + return 0; +} + +void Subscribe_GameEvents_Publish(const char[] sEventName, JSONObject jEvent) +{ + JSONObject jPublish = new JSONObject(); + jPublish.SetString("method", "publish"); + jPublish.SetString("module", "gameevents"); + jPublish.Set("event", jEvent); + + for(int Index = 0; Index < MAX_SUBSCRIBERS; Index++) + { + if(!g_Subscriber_Active[Index]) + continue; + + if(g_Subscriber_GameEvents[Index].FindString(sEventName) == -1) + continue; + + PublishEvent(g_Subscriber_Client[Index], jPublish); + } + + delete jPublish; +} +/* GameEvents */ diff --git a/SMJSONAPI/scripting/include/AsyncSocket.inc b/SMJSONAPI/scripting/include/AsyncSocket.inc new file mode 120000 index 00000000..575481cc --- /dev/null +++ b/SMJSONAPI/scripting/include/AsyncSocket.inc @@ -0,0 +1 @@ +../../../includes/AsyncSocket.inc \ No newline at end of file diff --git a/SMJSONAPI/scripting/include/smjansson.inc b/SMJSONAPI/scripting/include/smjansson.inc new file mode 120000 index 00000000..5e61c6c1 --- /dev/null +++ b/SMJSONAPI/scripting/include/smjansson.inc @@ -0,0 +1 @@ +../../../includes/smjansson.inc \ No newline at end of file