minecraft-pi-reborn/mods/src/server/server.cpp

541 lines
20 KiB
C++
Raw Normal View History

2020-10-10 23:02:13 +00:00
#include <string>
#include <stdint.h>
#include <ctime>
#include <cstdio>
#include <csignal>
#include <fstream>
2020-11-03 22:39:55 +00:00
#include <vector>
#include <pthread.h>
2020-10-10 23:02:13 +00:00
#include <unistd.h>
#include <SDL/SDL_events.h>
#include <libcore/libcore.h>
#include "server.h"
2020-11-06 04:05:37 +00:00
#include "server_internal.h"
2020-10-10 23:02:13 +00:00
#include "server_properties.h"
2020-11-06 04:05:37 +00:00
#include "playerdata.h"
2020-10-10 23:02:13 +00:00
// Server Properties
static ServerProperties &get_server_properties() {
static ServerProperties properties;
return properties;
}
// Default Server Properties
#define DEFAULT_MOTD "Minecraft Server"
2020-11-05 01:12:48 +00:00
#define DEFAULT_SHOW_MINECON_ICON "false"
2020-10-10 23:02:13 +00:00
#define DEFAULT_GAME_MODE "0"
#define DEFAULT_PORT "19132"
#define DEFAULT_SEED ""
#define DEFAULT_MOB_SPAWNING "true"
#define DEFAULT_WORLD_NAME "world"
2020-10-11 19:38:48 +00:00
#define DEFAULT_MAX_PLAYERS "4"
2020-10-10 23:02:13 +00:00
// Read STDIN Thread
static volatile bool stdin_buffer_complete = false;
static volatile char *stdin_buffer = NULL;
static void *read_stdin_thread(__attribute__((unused)) void *data) {
while (1) {
if (!stdin_buffer_complete) {
int x = getchar();
if (x != EOF) {
if (x == '\n') {
if (stdin_buffer == NULL) {
stdin_buffer = strdup("");
}
stdin_buffer_complete = true;
} else {
if (stdin_buffer == NULL) {
asprintf((char **) &stdin_buffer, "%c", (char) x);
} else {
asprintf((char **) &stdin_buffer, "%s%c", stdin_buffer, (char) x);
}
}
}
}
}
}
2020-11-04 21:05:31 +00:00
typedef void (*Minecraft_update_t)(unsigned char *minecraft);
static Minecraft_update_t Minecraft_update = (Minecraft_update_t) 0x16b74;
struct LevelSettings {
unsigned long seed;
int32_t game_type;
};
typedef void (*Minecraft_selectLevel_t)(unsigned char *minecraft, std::string const& level_dir, std::string const& level_name, LevelSettings const& vsettings);
static Minecraft_selectLevel_t Minecraft_selectLevel = (Minecraft_selectLevel_t) 0x16f38;
typedef void (*Minecraft_hostMultiplayer_t)(unsigned char *minecraft, int32_t port);
static Minecraft_hostMultiplayer_t Minecraft_hostMultiplayer = (Minecraft_hostMultiplayer_t) 0x16664;
typedef void *(*ProgressScreen_t)(unsigned char *obj);
static ProgressScreen_t ProgressScreen = (ProgressScreen_t) 0x37044;
typedef void (*Minecraft_setScreen_t)(unsigned char *minecraft, unsigned char *screen);
static Minecraft_setScreen_t Minecraft_setScreen = (Minecraft_setScreen_t) 0x15d6c;
2020-11-06 04:05:37 +00:00
// Get World Name
std::string server_internal_get_world_name() {
return get_server_properties().get_string("world-name", DEFAULT_WORLD_NAME);
}
2020-11-03 22:39:55 +00:00
// Create/Start World
static void start_world(unsigned char *minecraft) {
INFO("%s", "Starting Minecraft: Pi Edition Dedicated Server");
2020-11-03 22:39:55 +00:00
LevelSettings settings;
settings.game_type = get_server_properties().get_int("game-mode", DEFAULT_GAME_MODE);;
std::string seed_str = get_server_properties().get_string("seed", DEFAULT_SEED);
int32_t seed = seed_str.length() > 0 ? std::stoi(seed_str) : time(NULL);
settings.seed = seed;
2020-10-10 23:02:13 +00:00
2020-11-06 04:05:37 +00:00
std::string world_name = server_internal_get_world_name();
2020-11-03 22:39:55 +00:00
(*Minecraft_selectLevel)(minecraft, world_name, world_name, settings);
2020-10-10 23:02:13 +00:00
2020-11-03 22:39:55 +00:00
int port = get_server_properties().get_int("port", DEFAULT_PORT);
(*Minecraft_hostMultiplayer)(minecraft, port);
INFO("Listening On: %i", port);
2020-10-10 23:02:13 +00:00
2020-11-03 22:39:55 +00:00
void *screen = ::operator new(0x4c);
screen = (*ProgressScreen)((unsigned char *) screen);
(*Minecraft_setScreen)(minecraft, (unsigned char *) screen);
}
2020-10-10 23:02:13 +00:00
2020-11-04 00:31:27 +00:00
typedef const char *(*Minecraft_getProgressMessage_t)(unsigned char *minecraft);
static Minecraft_getProgressMessage_t Minecraft_getProgressMessage = (Minecraft_getProgressMessage_t) 0x16e58;
typedef int32_t (*Minecraft_isLevelGenerated_t)(unsigned char *minecraft);
static Minecraft_isLevelGenerated_t Minecraft_isLevelGenerated = (Minecraft_isLevelGenerated_t) 0x16e6c;
#define SIGNIFICANT_PROGRESS 5
// Check If Two Percentages Are Different Enough To Be Logged
static bool is_progress_difference_significant(int32_t new_val, int32_t old_val) {
if (new_val != old_val) {
if (new_val == -1 || old_val == -1) {
return true;
} else if (new_val == 0 || new_val == 100) {
return true;
} else {
return new_val - old_val >= SIGNIFICANT_PROGRESS;
}
} else {
return false;
}
}
2020-11-03 22:39:55 +00:00
// Print Progress Reports
static int last_progress = -1;
static const char *last_message = NULL;
static void print_progress(unsigned char *minecraft) {
2020-10-10 23:02:13 +00:00
const char *message = (*Minecraft_getProgressMessage)(minecraft);
int32_t progress = *(int32_t *) (minecraft + 0xc60);
if ((*Minecraft_isLevelGenerated)(minecraft)) {
message = "Ready";
progress = -1;
}
if (message != NULL) {
bool message_different = message != last_message;
bool progress_significant = is_progress_difference_significant(progress, last_progress);
if (message_different || progress_significant) {
if (progress != -1) {
INFO("Status: %s: %i%%", message, progress);
} else {
INFO("Status: %s", message);
}
if (message_different) {
last_message = message;
}
if (progress_significant) {
last_progress = progress;
}
}
}
2020-11-03 22:39:55 +00:00
}
2020-10-10 23:02:13 +00:00
2020-11-03 22:39:55 +00:00
struct RakNet_RakNetGUID {
2020-11-04 21:05:31 +00:00
unsigned char data[10];
2020-11-03 22:39:55 +00:00
};
struct RakNet_SystemAddress {
2020-11-04 21:05:31 +00:00
unsigned char data[20];
2020-11-03 22:39:55 +00:00
};
typedef RakNet_SystemAddress (*RakNet_RakPeer_GetSystemAddressFromGuid_t)(unsigned char *rak_peer, RakNet_RakNetGUID guid);
2020-11-03 22:39:55 +00:00
typedef void (*ServerSideNetworkHandler_displayGameMessage_t)(unsigned char *server_side_network_handler, std::string const& message);
static ServerSideNetworkHandler_displayGameMessage_t ServerSideNetworkHandler_displayGameMessage = (ServerSideNetworkHandler_displayGameMessage_t) 0x750c4;
2020-11-03 22:39:55 +00:00
typedef char *(*RakNet_SystemAddress_ToString_t)(RakNet_SystemAddress *system_address, bool print_delimiter, char delimiter);
static RakNet_SystemAddress_ToString_t RakNet_SystemAddress_ToString = (RakNet_SystemAddress_ToString_t) 0xd6198;
static std::string get_banned_ips_file() {
std::string file(getenv("HOME"));
file.append("/.minecraft/banned-ips.txt");
return file;
}
typedef void (*player_callback_t)(unsigned char *minecraft, std::string username, unsigned char *player);
2020-11-06 04:05:37 +00:00
// Get Vector Of Players In Level
std::vector<unsigned char *> server_internal_get_players(unsigned char *level) {
return *(std::vector<unsigned char *> *) (level + 0x60);
}
// Get Player's Username
std::string server_internal_get_player_username(unsigned char *player) {
return *(char **) (player + 0xbf4);
}
// Get Level From Minecraft
unsigned char *server_internal_get_level(unsigned char *minecraft) {
return *(unsigned char **) (minecraft + 0x188);
}
2020-11-03 22:39:55 +00:00
// Find Players With Username And Run Callback
2020-11-04 00:31:27 +00:00
static void find_players(unsigned char *minecraft, std::string target_username, player_callback_t callback, bool all_players) {
2020-11-06 04:05:37 +00:00
unsigned char *level = server_internal_get_level(minecraft);
std::vector<unsigned char *> players = server_internal_get_players(level);
2020-11-03 22:39:55 +00:00
bool found_player = false;
for (std::size_t i = 0; i < players.size(); i++) {
// Iterate Players
unsigned char *player = players[i];
2020-11-06 04:05:37 +00:00
std::string username = server_internal_get_player_username(player);
2020-11-04 00:31:27 +00:00
if (all_players || username == target_username) {
2020-11-03 22:39:55 +00:00
// Run Callback
(*callback)(minecraft, username, player);
found_player = true;
}
}
2020-11-04 00:31:27 +00:00
if (!all_players && !found_player) {
2020-11-03 22:39:55 +00:00
INFO("Invalid Player: %s", target_username.c_str());
}
}
2020-11-04 21:05:31 +00:00
static RakNet_RakNetGUID *get_rak_net_guid(unsigned char *player) {
return (RakNet_RakNetGUID *) (player + 0xc08);
}
static RakNet_SystemAddress get_system_address(unsigned char *rak_peer, RakNet_RakNetGUID guid) {
2020-11-03 22:39:55 +00:00
unsigned char *rak_peer_vtable = *(unsigned char **) rak_peer;
RakNet_RakPeer_GetSystemAddressFromGuid_t RakNet_RakPeer_GetSystemAddressFromGuid = *(RakNet_RakPeer_GetSystemAddressFromGuid_t *) (rak_peer_vtable + 0xd0);
// Get SystemAddress
2020-11-04 21:05:31 +00:00
return (*RakNet_RakPeer_GetSystemAddressFromGuid)(rak_peer, guid);
}
static unsigned char *get_rak_peer(unsigned char *minecraft) {
unsigned char *rak_net_instance = *(unsigned char **) (minecraft + 0x170);
return *(unsigned char **) (rak_net_instance + 0x4);
}
// Get IP From Player
static char *get_player_ip(unsigned char *minecraft, unsigned char *player) {
RakNet_RakNetGUID guid = *get_rak_net_guid(player);
unsigned char *rak_peer = get_rak_peer(minecraft);
RakNet_SystemAddress address = get_system_address(rak_peer, guid);
2020-11-03 22:39:55 +00:00
// Get IP
return (*RakNet_SystemAddress_ToString)(&address, false, '|');
}
// Ban Player
static void ban_callback(unsigned char *minecraft, std::string username, unsigned char *player) {
// Get IP
char *ip = get_player_ip(minecraft, player);
// Ban Player
INFO("Banned: %s (%s)", username.c_str(), ip);
// Write To File
std::ofstream banned_ips_output(get_banned_ips_file(), std::ios_base::app);
if (banned_ips_output) {
if (banned_ips_output.good()) {
banned_ips_output << ip <<'\n';
}
if (banned_ips_output.is_open()) {
banned_ips_output.close();
}
}
2020-10-10 23:02:13 +00:00
}
2020-11-03 22:39:55 +00:00
// Kill Player
typedef void (*Entity_die_t)(unsigned char *entity, unsigned char *cause);
static void kill_callback(__attribute__((unused)) unsigned char *minecraft, __attribute__((unused)) std::string username, unsigned char *player) {
unsigned char *player_vtable = *(unsigned char **) player;
Entity_die_t Entity_die = *(Entity_die_t *) (player_vtable + 0x130);
(*Entity_die)(player, NULL);
INFO("Killed: %s", username.c_str());
}
// List Player
static void list_callback(unsigned char *minecraft, std::string username, unsigned char *player) {
INFO(" - %s (%s)", username.c_str(), get_player_ip(minecraft, player));
}
2020-10-10 23:02:13 +00:00
typedef void (*Level_saveLevelData_t)(unsigned char *level);
static Level_saveLevelData_t Level_saveLevelData = (Level_saveLevelData_t) 0xa2e94;
static void Level_saveLevelData_injection(unsigned char *level) {
// Print Log Message
INFO("%s", "Saving Game");
// Call Original Method
(*Level_saveLevelData)(level);
2020-11-06 04:05:37 +00:00
// Save Player Data
playerdata_save(level);
2020-10-10 23:02:13 +00:00
}
2020-11-04 21:05:31 +00:00
typedef void (*Minecraft_leaveGame_t)(unsigned char *minecraft, bool save_remote_level);
static Minecraft_leaveGame_t Minecraft_leaveGame = (Minecraft_leaveGame_t) 0x15ea0;
2020-11-03 22:39:55 +00:00
// Stop Server
2020-11-04 21:05:31 +00:00
static bool exit_requested = false;
2020-11-03 22:39:55 +00:00
static void exit_handler(__attribute__((unused)) int data) {
2020-11-04 21:05:31 +00:00
exit_requested = true;
}
static void handle_server_stop(unsigned char *minecraft) {
if (exit_requested) {
INFO("%s", "Stopping Server");
// Save And Exit
2020-11-06 04:05:37 +00:00
unsigned char *level = server_internal_get_level(minecraft);
2020-11-10 02:31:02 +00:00
if (level != NULL) {
Level_saveLevelData_injection(level);
}
2020-11-04 21:05:31 +00:00
(*Minecraft_leaveGame)(minecraft, false);
// Stop Game
SDL_Event event;
event.type = SDL_QUIT;
SDL_PushEvent(&event);
exit_requested = false;
2020-11-03 22:39:55 +00:00
}
2020-11-04 21:05:31 +00:00
}
// Get ServerSideNetworkHandler From Minecraft
static unsigned char *get_server_side_network_handler(unsigned char *minecraft) {
return *(unsigned char **) (minecraft + 0x174);
2020-11-03 22:39:55 +00:00
}
// Handle Commands
static void handle_commands(unsigned char *minecraft) {
2020-11-04 21:05:31 +00:00
if ((*Minecraft_isLevelGenerated)(minecraft) && stdin_buffer_complete) {
2020-11-03 22:39:55 +00:00
if (stdin_buffer != NULL) {
2020-11-04 21:05:31 +00:00
unsigned char *server_side_network_handler = get_server_side_network_handler(minecraft);
2020-11-03 22:39:55 +00:00
if (server_side_network_handler != NULL) {
std::string data((char *) stdin_buffer);
static std::string ban_command("ban ");
static std::string say_command("say ");
static std::string kill_command("kill ");
static std::string list_command("list");
static std::string stop_command("stop");
static std::string help_command("help");
if (data.rfind(ban_command, 0) == 0) {
// IP-Ban Target Username
std::string ban_username = data.substr(ban_command.length());
2020-11-04 00:31:27 +00:00
find_players(minecraft, ban_username, ban_callback, false);
2020-11-03 22:39:55 +00:00
} else if (data.rfind(kill_command, 0) == 0) {
// Kill Target Username
std::string kill_username = data.substr(kill_command.length());
2020-11-04 00:31:27 +00:00
find_players(minecraft, kill_username, kill_callback, false);
2020-11-03 22:39:55 +00:00
} else if (data.rfind(say_command, 0) == 0) {
// Format Message
std::string message = "[Server] " + data.substr(say_command.length());
// Post Message To Chat
(*ServerSideNetworkHandler_displayGameMessage)(server_side_network_handler, message);
} else if (data == list_command) {
// List Players
INFO("%s", "All Players:");
2020-11-04 00:31:27 +00:00
find_players(minecraft, "", list_callback, true);
2020-11-03 22:39:55 +00:00
} else if (data == stop_command) {
// Stop Server
exit_handler(-1);
} else if (data == help_command) {
INFO("%s", "All Commands:");
INFO("%s", " ban <Username> - IP-Ban All Players With Specifed Username");
INFO("%s", " kill <Username> - Kill All Players With Specifed Username");
INFO("%s", " say <Message> - Print Specified Message To Chat");
INFO("%s", " list - List All Players");
INFO("%s", " stop - Stop Server");
INFO("%s", " help - Print This Message");
} else {
INFO("Invalid Command: %s", data.c_str());
}
}
free((void *) stdin_buffer);
stdin_buffer = NULL;
}
stdin_buffer_complete = false;
}
}
// Runs Every Tick
static bool loaded = false;
static void Minecraft_update_injection(unsigned char *minecraft) {
// Create/Start World
if (!loaded) {
start_world(minecraft);
loaded = true;
}
// Print Progress Reports
print_progress(minecraft);
// Call Original Method
(*Minecraft_update)(minecraft);
// Handle Commands
handle_commands(minecraft);
2020-11-04 21:05:31 +00:00
// Server Stop
handle_server_stop(minecraft);
2020-11-03 22:39:55 +00:00
}
2020-10-10 23:02:13 +00:00
typedef void (*Gui_addMessage_t)(unsigned char *gui, std::string const& text);
static Gui_addMessage_t Gui_addMessage = (Gui_addMessage_t) 0x27820;
static void Gui_addMessage_injection(unsigned char *gui, std::string const& text) {
// Print Log Message
2020-10-10 23:39:29 +00:00
fprintf(stderr, "[CHAT]: %s\n", text.c_str());
2020-11-03 22:39:55 +00:00
2020-10-10 23:02:13 +00:00
// Call Original Method
(*Gui_addMessage)(gui, text);
}
2020-11-03 22:39:55 +00:00
typedef bool (*RakNet_RakPeer_IsBanned_t)(unsigned char *rakpeer, const char *ip);
static RakNet_RakPeer_IsBanned_t RakNet_RakPeer_IsBanned = (RakNet_RakPeer_IsBanned_t) 0xda3b4;
static bool RakNet_RakPeer_IsBanned_injection(__attribute__((unused)) unsigned char *rakpeer, const char *ip) {
// Check banned-ips.txt
std::string banned_ips_file_path = get_banned_ips_file();
std::ifstream banned_ips_file(banned_ips_file_path);
if (banned_ips_file) {
bool ret = false;
if (banned_ips_file.good()) {
std::string line;
while (std::getline(banned_ips_file, line)) {
if (line.length() > 0) {
if (line[0] == '#') {
continue;
}
if (strcmp(line.c_str(), ip) == 0) {
ret = true;
break;
2020-11-03 22:39:55 +00:00
}
}
}
2020-10-10 23:02:13 +00:00
}
if (banned_ips_file.is_open()) {
banned_ips_file.close();
}
return ret;
} else {
ERR("%s", "Unable To Read banned-ips.txt");
2020-10-10 23:02:13 +00:00
}
}
const char *server_get_motd() {
2020-11-10 02:31:02 +00:00
std::string *motd = new std::string(get_server_properties().get_string("motd", DEFAULT_MOTD));
return motd->c_str();
2020-10-10 23:02:13 +00:00
}
int server_get_mob_spawning() {
return get_server_properties().get_bool("spawn-mobs", DEFAULT_MOB_SPAWNING);
}
2020-10-11 19:38:48 +00:00
static unsigned char server_get_max_players() {
int val = get_server_properties().get_int("max-players", DEFAULT_MAX_PLAYERS);
if (val < 0) {
val = 0;
}
if (val > 255) {
val = 255;
}
return (unsigned char) val;
}
2020-10-10 23:02:13 +00:00
void server_init() {
// Open Properties File
std::string file(getenv("HOME"));
file.append("/.minecraft/server.properties");
std::ifstream properties_file(file);
2020-11-03 22:39:55 +00:00
if (!properties_file || !properties_file.good()) {
2020-10-10 23:02:13 +00:00
// Write Defaults
std::ofstream properties_file_output(file);
2020-10-14 17:40:32 +00:00
properties_file_output << "# Message Of The Day\n";
2020-10-10 23:02:13 +00:00
properties_file_output << "motd=" DEFAULT_MOTD "\n";
2020-11-05 01:12:48 +00:00
properties_file_output << "# Show The MineCon Icon Next To MOTD In Server List\n";
properties_file_output << "show-minecon-icon=" DEFAULT_SHOW_MINECON_ICON "\n";
2020-10-14 17:40:32 +00:00
properties_file_output << "# Game Mode (0 = Survival, 1 = Creative)\n";
2020-10-10 23:02:13 +00:00
properties_file_output << "game-mode=" DEFAULT_GAME_MODE "\n";
2020-10-14 17:40:32 +00:00
properties_file_output << "# Port\n";
2020-10-10 23:02:13 +00:00
properties_file_output << "port=" DEFAULT_PORT "\n";
2020-10-14 17:40:32 +00:00
properties_file_output << "# World Seed (Blank = Random Seed)\n";
2020-10-10 23:02:13 +00:00
properties_file_output << "seed=" DEFAULT_SEED "\n";
2020-10-14 17:40:32 +00:00
properties_file_output << "# Mob Spawning (false = Disabled, true = Enabled)\n";
2020-10-10 23:02:13 +00:00
properties_file_output << "spawn-mobs=" DEFAULT_MOB_SPAWNING "\n";
2020-10-14 17:40:32 +00:00
properties_file_output << "# World To Select\n";
2020-10-10 23:02:13 +00:00
properties_file_output << "world-name=" DEFAULT_WORLD_NAME "\n";
2020-10-15 03:23:31 +00:00
properties_file_output << "# Maximum Player Count\n";
2020-10-11 19:38:48 +00:00
properties_file_output << "max-players=" DEFAULT_MAX_PLAYERS "\n";
2020-10-10 23:02:13 +00:00
properties_file_output.close();
// Re-Open File
properties_file = std::ifstream(file);
}
if (!properties_file.is_open()) {
2020-10-26 19:58:28 +00:00
ERR("%s", "Unable To Open server.properties");
2020-10-10 23:02:13 +00:00
}
// Load Properties
get_server_properties().load(properties_file);
properties_file.close();
2020-11-03 22:39:55 +00:00
// Create Empty Banned IPs File
std::string banned_ips_file_path = get_banned_ips_file();
std::ifstream banned_ips_file(banned_ips_file_path);
if (!banned_ips_file || !banned_ips_file.good()) {
// Write Default
std::ofstream banned_ips_output(banned_ips_file_path);
banned_ips_output << "# List Of Banned IPs; Each Line Is One IP Address\n";
banned_ips_output.close();
}
if (banned_ips_file.is_open()) {
banned_ips_file.close();
}
2020-10-10 23:02:13 +00:00
// Prevent Main Player From Loading
unsigned char player_patch[4] = {0x00, 0x20, 0xa0, 0xe3};
patch((void *) 0x1685c, player_patch);
// Start World On Launch
overwrite_calls((void *) Minecraft_update, (void *) Minecraft_update_injection);
2020-10-10 23:02:13 +00:00
// Print Log On Game Save
overwrite_calls((void *) Level_saveLevelData, (void *) Level_saveLevelData_injection);
2020-10-10 23:02:13 +00:00
// Exit handler
signal(SIGINT, exit_handler);
// Print Chat To Log
overwrite_calls((void *) Gui_addMessage, (void *) Gui_addMessage_injection);
// Allow All IPs To Join
2020-10-11 18:23:22 +00:00
unsigned char allow_all_ip_patch[4] = {0x00, 0xf0, 0x20, 0xe3};
patch((void *) 0xe1f6c, allow_all_ip_patch);
2020-10-11 19:38:48 +00:00
// Set Max Players
unsigned char max_players_patch[4] = {server_get_max_players(), 0x30, 0xa0, 0xe3};
patch((void *) 0x166d0, max_players_patch);
2020-11-03 22:39:55 +00:00
// Custom Banned IP List
overwrite((void *) RakNet_RakPeer_IsBanned, (void *) RakNet_RakPeer_IsBanned_injection);
2020-11-06 04:05:37 +00:00
// Load Player Data
playerdata_init();
2020-11-05 01:12:48 +00:00
if (get_server_properties().get_bool("show-minecon-icon", DEFAULT_SHOW_MINECON_ICON)) {
// Show The MineCon Icon Next To MOTD In Server List
unsigned char minecon_icon_patch[4] = {0x04, 0x1a, 0x9f, 0xe5};
patch((void *) 0x737e4, minecon_icon_patch);
}
// Start Reading STDIN
pthread_t read_stdin_thread_obj;
pthread_create(&read_stdin_thread_obj, NULL, read_stdin_thread, NULL);
2020-10-10 23:02:13 +00:00
}