Files
sfall/sfall/Modules/DebugEditor.cpp
T

558 lines
14 KiB
C++

/*
* sfall
* Copyright (C) 2008, 2009, 2010 The sfall team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <vector>
#include "..\main.h"
#include "..\FalloutEngine\Fallout2.h"
#include "..\InputFuncs.h"
//#include "Graphics.h"
#include "LoadGameHook.h"
#include "ScriptExtender.h"
#include "Scripting\Arrays.h"
#include "DebugEditor.h"
namespace sfall
{
enum DECode {
CODE_SET_GLOBAL = 0,
CODE_SET_MAPVAR = 1,
CODE_GET_CRITTER = 2,
CODE_SET_CRITTER = 3,
CODE_SET_SGLOBAL = 4,
CODE_GET_PROTO = 5,
CODE_SET_PROTO = 6,
CODE_GET_PLAYER = 7,
CODE_SET_PLAYER = 8,
CODE_GET_ARRAY = 9,
CODE_SET_ARRAY = 10,
CODE_GET_LOCVARS = 11,
CODE_SET_LOCVARS = 12,
CODE_EXIT = 254
};
static const char* debugLog = "LOG";
static const char* debugGnw = "GNW";
static DWORD debugEditorKey;
static DWORD mapGridToggleKey;
struct sArray {
DWORD id;
long isMap;
long size;
long flag;
};
static bool SetBlocking(SOCKET s, bool block) {
DWORD d = !block;
ioctlsocket(s, FIONBIO, &d);
}
static bool InternalSend(SOCKET s, const void* _data, int size) {
const char* data = (const char*)_data;
int upto = 0;
while (upto < size) {
int tmp = send(s, &data[upto], size - upto, 0);
if (tmp > 0) {
upto += tmp;
} else {
DWORD d = WSAGetLastError();
if (d != WSAEWOULDBLOCK && d != WSAENOBUFS) return true;
}
}
return false;
}
static bool InternalRecv(SOCKET s, void* _data, int size) {
char* data = (char*)_data;
int upto = 0;
while (upto < size) {
int tmp = recv(s, &data[upto], size - upto, 0);
if (tmp > 0) {
upto += tmp;
} else {
DWORD d = WSAGetLastError();
if (d != WSAEWOULDBLOCK && d != WSAENOBUFS) return true;
}
}
return false;
}
static void RunEditorInternal(SOCKET &s) {
fo::var::setInt(FO_VAR_script_engine_running) = 0;
std::vector<DWORD*> vec = std::vector<DWORD*>();
for (int elv = 0; elv < 3; elv++) {
for (int tile = 0; tile < 40000; tile++) {
fo::GameObject* obj = fo::func::obj_find_first_at_tile(elv, tile);
while (obj) {
if (obj->IsCritter()) {
vec.push_back(reinterpret_cast<DWORD*>(obj));
}
obj = fo::func::obj_find_next_at_tile();
}
}
}
int numCritters = vec.size();
int numGlobals = *fo::ptr::num_game_global_vars;
int numMapVars = *fo::ptr::num_map_global_vars;
int numSGlobals = GetNumGlobals();
int numArrays = script::GetNumArrays();
InternalSend(s, &numGlobals, 4);
InternalSend(s, &numMapVars, 4);
InternalSend(s, &numSGlobals, 4);
InternalSend(s, &numArrays, 4);
InternalSend(s, &numCritters, 4);
GlobalVar* sglobals = new GlobalVar[numSGlobals];
GetGlobals(sglobals);
sArray* arrays = new sArray[numArrays];
script::GetArrays((int*)arrays);
InternalSend(s, reinterpret_cast<void*>(*fo::ptr::game_global_vars), 4 * numGlobals);
InternalSend(s, reinterpret_cast<void*>(*fo::ptr::map_global_vars), 4 * numMapVars);
InternalSend(s, sglobals, sizeof(GlobalVar) * numSGlobals);
InternalSend(s, arrays, numArrays * sizeof(sArray));
for (int i = 0; i < numCritters; i++) {
InternalSend(s, &vec[i][25], 4);
InternalSend(s, &vec[i], 4);
}
while (true) {
BYTE code;
InternalRecv(s, &code, 1);
if (code == CODE_EXIT) break;
int id, val;
switch (code) {
case CODE_SET_GLOBAL:
InternalRecv(s, &id, 4);
InternalRecv(s, &val, 4);
(*fo::ptr::game_global_vars)[id] = val;
break;
case CODE_SET_MAPVAR:
InternalRecv(s, &id, 4);
InternalRecv(s, &val, 4);
(*fo::ptr::map_global_vars)[id] = val;
break;
case CODE_GET_CRITTER:
InternalRecv(s, &id, 4);
InternalSend(s, vec[id], 0x84);
break;
case CODE_SET_CRITTER:
InternalRecv(s, &id, 4);
InternalRecv(s, vec[id], 0x84);
break;
case CODE_SET_SGLOBAL:
InternalRecv(s, &id, 4);
InternalRecv(s, &val, 4);
sglobals[id].val = val;
break;
case CODE_GET_ARRAY: // get array values
{
InternalRecv(s, &id, 4);
DWORD *types = new DWORD[arrays[id].size * 2]; // type, len
script::DEGetArray(arrays[id].id, types, nullptr);
int dataLen = 0;
for (long i = 0; i < arrays[id].size; i++) {
dataLen += types[i * 2 + 1];
}
char *data = new char[dataLen];
script::DEGetArray(arrays[id].id, nullptr, data);
InternalSend(s, types, arrays[id].size * 8);
InternalSend(s, data, dataLen);
delete[] data;
delete[] types;
}
break;
case CODE_SET_ARRAY: // set array values
{
InternalRecv(s, &id, 4);
InternalRecv(s, &val, 4); // len data
char *data = new char[val];
InternalRecv(s, data, val);
script::DESetArray(arrays[id].id, nullptr, data);
delete[] data;
}
break;
case CODE_GET_LOCVARS:
{
InternalRecv(s, &id, 4); // sid
val = fo::util::GetScriptLocalVars(id);
InternalSend(s, &val, 4);
if (val) {
std::vector<int> values(val);
long varVal;
for (int i = 0; i < val; i++) {
fo::func::scr_get_local_var(id, i, &varVal);
values[i] = varVal;
}
InternalSend(s, values.data(), val * 4);
}
}
break;
case CODE_SET_LOCVARS:
{
InternalRecv(s, &id, 4); // sid
InternalRecv(s, &val, 4); // len data
std::vector<int> values(val);
InternalRecv(s, values.data(), val * 4);
for (int i = 0; i < val; i++) {
fo::func::scr_set_local_var(id, i, values[i]);
}
}
break;
}
__asm call fo::funcoffs::GNW95_process_message_;
}
SetGlobals(sglobals);
delete[] sglobals;
delete[] arrays;
FlushInputBuffer();
fo::var::setInt(FO_VAR_script_engine_running) = 1;
}
void RunDebugEditor() {
WSADATA wsaData;
SOCKET sock, client;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != NO_ERROR) return;
// create the socket
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
WSACleanup();
return;
}
// bind the socket
sockaddr_in service;
service.sin_family = AF_INET;
service.sin_addr.s_addr = inet_addr("127.0.0.1");
service.sin_port = htons(4245);
if (bind(sock, (SOCKADDR*)&service, sizeof(service)) == SOCKET_ERROR) {
closesocket(sock);
WSACleanup();
return;
}
if (listen(sock, 4) == SOCKET_ERROR) {
closesocket(sock);
WSACleanup();
return;
}
// Start up the editor
STARTUPINFO si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);
if (!CreateProcessA("FalloutDebug.exe", "FalloutDebug.exe -debugedit", 0, 0, false, 0, 0, 0, &si, &pi)) {
closesocket(sock);
WSACleanup();
return;
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
// Connect to the editor
client = accept(sock, 0, 0);
if (client == SOCKET_ERROR) {
closesocket(sock);
WSACleanup();
return;
}
MouseDeviceUnacquire(true);
RunEditorInternal(client);
closesocket(client);
closesocket(sock);
WSACleanup();
}
static void __declspec(naked) dbg_error_hack() {
static const DWORD dbg_error_ret = 0x453FD8;
__asm {
cmp ebx, 1;
je hide;
sub esp, 0x104;
jmp dbg_error_ret;
hide:
pop esi;
pop ecx;
retn;
}
}
static void __declspec(naked) art_data_size_hook() {
static const char* artDbgMsg = "\nERROR: Art file not found: %s\n";
__asm {
test edi, edi; // 1 - isExistsArt
jz artNotExist;
retn;
artNotExist:
mov edx, [esp + 0x124 - 0x1C + 4]; // filename
push edx;
push artDbgMsg;
call fo::funcoffs::debug_printf_;
cmp isDebug, 0;
jne display;
add esp, 8;
retn;
display:
push edx; // filename
push artDbgMsg;
lea eax, [esp + 0x124 - 0x124 + 20]; // buf
push eax;
call fo::funcoffs::sprintf_;
add esp, 20;
lea eax, [esp + 4];
jmp fo::funcoffs::display_print_;
}
}
static void __declspec(naked) art_data_size_hook_check() {
using namespace fo;
__asm {
xor esi, esi;
mov eax, ebx; // ebx - FID
shr eax, 16; // al - animation code (ID2)
cmp al, ANIM_walk;
cmove ecx, esi;
retn;
}
}
static void __declspec(naked) proto_load_pid_hack() {
static const char* proDbgMsg = "\nERROR: Reading prototype file: %s\n";
__asm {
mov dword ptr [esp + 0x120 - 0x1C + 4], -1;
lea eax, [esp + 0x120 - 0x120 + 4]; // pro file
push eax;
push proDbgMsg;
call fo::funcoffs::debug_printf_;
add esp, 8;
mov eax, 0x500494; // 'iisxxxx1'
jmp fo::funcoffs::gsound_play_sfx_file_;
}
}
static void __declspec(naked) win_debug_hook() {
__asm {
call fo::funcoffs::debug_log_;
xor eax, eax;
cmp ds:[FO_VAR_GNW_win_init_flag], eax;
retn;
}
}
static void __declspec(naked) debug_log_hack() {
__asm {
push eax; // text
push 0x5016EC; // fmt '%s'
push ebx; // log file
call fo::funcoffs::fprintf_;
add esp, 12;
retn;
}
}
static void __declspec(naked) debugMsg() {
static const char* scrNameFmt = "\nScript: %s ";
__asm {
mov edx, ds:[FO_VAR_currentProgram];
push [edx]; // script name
push scrNameFmt;
call fo::funcoffs::debug_printf_;
add esp, 8;
jmp fo::funcoffs::debug_printf_; // ERROR: attempt to reference...
}
}
static void __declspec(naked) combat_load_hack() {
static const char* msgCombat = "LOADSAVE: Object ID not found while loading the combat data.\n";
__asm {
push msgCombat;
call fo::funcoffs::debug_printf_;
add esp, 4;
mov eax, -1;
retn;
}
}
// Shifts the string one character to the right and inserts a newline control character at the beginning
static void MoveDebugString(char* messageAddr) {
int i = 0;
do {
messageAddr[i + 1] = messageAddr[i];
i--;
} while (messageAddr[i] != '\0');
messageAddr[i + 1] = '\n';
}
static const DWORD addrDotChar[] = {
0x50B244, 0x50B27C, 0x50B2B6, 0x50B2EE // "ERROR: attempt to reference * var out of range: %d"
};
static const DWORD addrNewLineChar[] = {
0x500A64, // "Friendly was in the way!"
};
static void DebugModePatch() {
int dbgMode = IniReader::GetIntDefaultConfig("Debugging", "DebugMode", 0);
if (dbgMode > 0) {
dlog("Applying debugmode patch.", DL_INIT);
// If the player is using an exe with the debug patch already applied, just skip this block without erroring
if (*((DWORD*)0x444A64) != 0x082327E8) {
SafeWrite32(0x444A64, 0x082327E8); // call debug_register_env_
SafeWrite32(0x444A68, 0x0120E900); // jmp 0x444B8E
SafeWrite8(0x444A6D, 0);
SafeWrite32(0x444A6E, 0x90909090);
}
SafeWrite8(0x4C6D9B, 0xB8); // mov eax, GNW/LOG
if (dbgMode & 2) {
SafeWrite32(0x4C6D9C, (DWORD)debugLog);
if (dbgMode & 1) {
SafeWrite16(0x4C6E75, 0x66EB); // jmps 0x4C6EDD
SafeWrite8(0x4C6EF2, CodeType::JumpShort);
SafeWrite8(0x4C7034, 0x0);
MakeCall(0x4DC319, win_debug_hook, 2);
}
} else {
SafeWrite32(0x4C6D9C, (DWORD)debugGnw);
}
if (IniReader::GetIntDefaultConfig("Debugging", "HideObjIsNullMsg", 0)) {
MakeJump(0x453FD2, dbg_error_hack);
}
// prints a debug message about a missing critter art file to both debug.log and the message window in sfall debugging mode
HookCall(0x419B65, art_data_size_hook);
// checks the animation code, if ANIM_walk then skip printing the debug message
HookCall(0x419AA0, art_data_size_hook_check);
SafeWrite8(0x419B61, CodeType::JumpNZ); // jz > jnz
// Fix to prevent crashes when there is a '%' character in the printed message
if (dbgMode > 1) {
MakeCall(0x4C703F, debug_log_hack);
BlockCall(0x4C7044); // just nop code
}
// replace calling debug_printf_ with _debug_func
__int64 data = 0x51DF0415FFF08990; // mov eax, esi; call ds:_debug_func
SafeWriteBytes(0x455419, (BYTE*)&data, 8); // op_display_msg_
// set the position of the debug window
SafeWrite8(0x4DC34D, 15);
// Fix the format of some debug messages
SafeWriteBatch<BYTE>('\n', addrNewLineChar);
SafeWriteBatch<BYTE>('.', addrDotChar);
const DWORD mapVarErrorAddr[] = {
0x482240, // map_set_global_var_
0x482274, // map_get_global_var_
0x4822A0, // map_set_local_var_
0x4822D4 // map_get_local_var_
};
HookCalls(debugMsg, mapVarErrorAddr);
if (dbgMode != 1) {
MoveDebugString((char*)0x500A9B); // "computing attack..."
}
dlogr(" Done", DL_INIT);
}
}
static void DontDeleteProtosPatch() {
if (IniReader::GetIntDefaultConfig("Debugging", "DontDeleteProtos", 0)) {
dlog("Applying permanent protos patch.", DL_INIT);
SafeWrite8(0x48007E, CodeType::JumpShort);
dlogr(" Done", DL_INIT);
}
}
/*
void CheckTimerResolution() {
DWORD ticksList[50];
DWORD old_ticks = GetTickCount();
for (size_t i = 0; i < 50;) {
DWORD ticks = GetTickCount();
if (ticks != old_ticks) {
old_ticks = ticks;
ticksList[i++] = ticks;
}
}
int min = 100, max = 0;
for (size_t i = 0; i < 49; i++) {
int ms = ticksList[i + 1] - ticksList[i];
if (ms < min) min = ms;
if (ms > max) max = ms;
}
fo::func::debug_printf("System timer resolution: %d - %d ms.\n", min, max);
}
*/
void DebugEditor::OnAfterGameInit() {
if (mapGridToggleKey) {
fo::var::setInt(FO_VAR_tile_refresh) = fo::funcoffs::refresh_mapper_;
//fo::var::setInt(FO_VAR_map_scroll_refresh) = fo::funcoffs::map_scroll_refresh_mapper_;
}
}
void DebugEditor::init() {
DebugModePatch();
// Notifies and prints a debug message about a corrupted proto file to debug.log
MakeCall(0x4A1D73, proto_load_pid_hack, 6);
// Prints a debug message about a missing combat object to debug.log
const DWORD combatLoadNullObjAddr[] = {0x421146, 0x421189, 0x4211CC};
MakeCalls(combat_load_hack, combatLoadNullObjAddr);
if (!isDebug) {
const DWORD combatLoadObjRetAddr[] = {0x42114B, 0x42118E, 0x4211D1};
SafeWriteBatch<DWORD>(0x909008EB, combatLoadObjRetAddr); // jmp $+8
}
if (!isDebug) return;
DontDeleteProtosPatch();
debugEditorKey = IniReader::GetConfigInt("Input", "DebugEditorKey", 0);
mapGridToggleKey = IniReader::GetIntDefaultConfig("Debugging", "MapGridToggleKey", 0);
}
void DebugEditor::KeyPressedHook(DWORD scanCode, bool pressed) {
if (debugEditorKey && scanCode == debugEditorKey && pressed && IsGameLoaded()) {
RunDebugEditor();
}
if (mapGridToggleKey && scanCode == mapGridToggleKey && pressed && IsGameLoaded()) {
__asm call fo::funcoffs::grid_toggle_;
fo::func::tile_refresh_display();
}
}
}