mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
121 lines
4.4 KiB
Text
121 lines
4.4 KiB
Text
ACS Virtual Machine (ACSVM)
|
|
|
|
ACS VM library and standalone interpreter. Intended to be suitable for use in
|
|
Doom-based video game engines to implement Hexen ACS or ZDoom ACS bytecode
|
|
execution. It is focused on being usable for implementing existing extensions
|
|
and new functions (whether through new instructions or CallFunc indexes) while
|
|
having performance suitable to the complex ACS-based mods that have come into
|
|
existence.
|
|
|
|
|
|
===============================================================================
|
|
Integrating ACSVM
|
|
===============================================================================
|
|
|
|
Although it can be used as an external library, ACSVM is also written so that
|
|
it can be integrated directly into the repository of projects using it without
|
|
needing to change any of the ACSVM files.
|
|
|
|
It is enough to copy ACSVM's root into the root of the target repository, such
|
|
that acsvm/CMakeLists.txt exists. Only the subdirectories of desired components
|
|
(usually just ACSVM) need to be included. The CMakeLists.txt will automatically
|
|
disable the omitted components.
|
|
|
|
In your root CMakeLists.txt, all that is needed is:
|
|
set(ACSVM_NOFLAGS ON)
|
|
set(ACSVM_SHARED OFF)
|
|
add_subdirectory(acsvm)
|
|
And the enabled components (again, usually just acsvm) will be available for
|
|
use in target_link_libraries.
|
|
|
|
|
|
===============================================================================
|
|
Usage Overview
|
|
===============================================================================
|
|
|
|
===========================================================
|
|
Getting Started
|
|
===========================================================
|
|
|
|
To use ACSVM, you will need to define a class that inherits from
|
|
ACSVM::Environment. By overriding the various virtuals you can configure the
|
|
different aspects of ACS loading and interpretation. But the absolute minimal
|
|
usage only requires overriding loadModule:
|
|
class Env : public ACSVM::Environment
|
|
{
|
|
protected:
|
|
virtual void loadModule(ACSVM::Module *module);
|
|
};
|
|
|
|
Which is implemented by using the module's name to locate the corresponding
|
|
bytecode and passing that to module->readBytecode. The default behavior of
|
|
getModuleName is to just set the ModuleName's string. This can be used to
|
|
implement bytecode directly from files:
|
|
void Env::loadModule(ACSVM::Module *module)
|
|
{
|
|
std::ifstream in{module->name.s->str,
|
|
std::ios_base::in | std::ios_base::binary};
|
|
|
|
if(!in) throw ACSVM::ReadError("file open failure");
|
|
|
|
std::vector<ACSVM::Byte> data;
|
|
|
|
for(int c; c = in.get(), in;)
|
|
data.push_back(c);
|
|
|
|
module->readBytecode(data.data(), data.size());
|
|
}
|
|
In a Doom engine, this would most likely use lumps, instead. Either by doing
|
|
the lookup in loadModule, or by overriding getModuleName to turn the input
|
|
string into a lump number.
|
|
|
|
To actually initialize the environment and load some modules, you can use:
|
|
void EnvInit(Environment &env, char const *const *namev, std::size_t namec)
|
|
{
|
|
// Load modules.
|
|
std::vector<ACSVM::Module *> modules;
|
|
for(std::size_t i = 1; i < namec; ++i)
|
|
modules.push_back(env.getModule(env.getModuleName(namev[i])));
|
|
|
|
// Create and activate scopes.
|
|
ACSVM::GlobalScope *global = env.getGlobalScope(0); global->active = true;
|
|
ACSVM::HubScope *hub = global->getHubScope(0); hub ->active = true;
|
|
ACSVM::MapScope *map = hub->getMapScope(0); map ->active = true;
|
|
|
|
// Register modules with map scope.
|
|
map->addModules(modules.data(), modules.size());
|
|
|
|
// Start Open scripts.
|
|
map->scriptStartType(1, {});
|
|
}
|
|
|
|
And then a simple interpreter loop:
|
|
while(env.hasActiveThread())
|
|
{
|
|
std::chrono::duration<double> rate{1.0 / 35};
|
|
auto time = std::chrono::steady_clock::now() + rate;
|
|
|
|
env.exec();
|
|
|
|
std::this_thread::sleep_until(time);
|
|
}
|
|
Note that if you already have a game loop, you only need to call env.exec once
|
|
per simulation frame.
|
|
|
|
Finally, you will need to register instruction and callfunc functions to
|
|
actually interface with the larger environment. At the least, it is useful to
|
|
implement the EndPrint (86) instruction:
|
|
bool CF_EndPrint(ACSVM::Thread *thread, ACSVM::Word const *, ACSVM::Word)
|
|
{
|
|
std::cout << thread->printBuf.data() << '\n';
|
|
thread->printBuf.drop();
|
|
return false;
|
|
}
|
|
|
|
Environment::Environment()
|
|
{
|
|
addCodeDataACS0(86, {"", 0, addCallFunc(CF_EndPrint)});
|
|
}
|
|
Most of the other ACS printing logic is already handled by ACSVM, so this is
|
|
enough to display simple Print messages.
|
|
|