-
Hi! Bear with me while I supply some context. I'm working on a C++ Multi-User-Dimension (essentially a text-based server game; think Zork), and I'm getting to a point where I'd like to implement a scripting engine. Essentially, I'd like the admin of the game to write generic behavior scripts that can be attached to mobiles, items or rooms. These scripts would include and adhere to a few essential interface functions that correspond to the host application's lifecycle events. For instance, a scripter would write in Chaiscript:
Where An example C++ host application utilizing the above script might look like:
And that works just as I'd expect! The problem occurs when multiple items exist, each with their own "onDamaged" (and perhaps other, similar, or same helper/lifecycle functions). As you can imagine, I get multiple definition errors because each of the items are attempting to define an I'm thinking chaiscript namespaces could solve this, but I'm unsure how to share a function from chaiscript to c++ that is in a Chaiscript namespace and evaluate it in C++. If you're following what I'm trying to do, I'd greatly appreciate tips on how to make it work! |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
Not sure I totally understand what you intend/expect, but one possibility might be to return (from your item script) either a Function, and array of Functions, or a data structure like a Map that contains named Functions, such as your Would your Item scripts potentially change (get re-evaluated) during the run of the program? (e.g. for live redefinition of behaviours) Although that should also be supported by what I suggest above. If you have a limited (and known in advance) number of such "callbacks," another approach would be to define (e.g. Not sure if that helps at all, or if I understood you... |
Beta Was this translation helpful? Give feedback.
-
Hi Aaron, what I mentioned in my earlier comment was an idea to return a Map of named functions for each loaded behaviour (or some other way to just return the behaviour data without defining it in global functions). Then you don't need to worry about prefixing things and unique names; instead you store them in a dictionary of named behaviours (each of which has any number of named "callback" functions). You don't store them in variables in the ChaiScript context, and you can redefine them (by loading again and updating the behaviour dictionary) whenever you want. Here is a moderately simple example of that technique (though complex enough to show a few more possibilities)...it's just one way (of many) that you might do it. Hopefully it'll give you some more ideas to explore. Note that real code should handle errors (e.g. try/catch around ChaiScript evaluation and casting of boxed values) -- my example was long enough as it was, I tried to keep it simple. I "load" the .chai files from in-memory strings for simplicity; you would just use You didn't provide your #include <string>
#include <chaiscript/chaiscript.hpp>
class Item {
public:
explicit Item(std::string const& name, int durability, std::string const& behaviorName)
: name{ name }
, durability{ durability }
, behaviorName{ behaviorName } {}
std::string getName() const { return name; }
int getDurability() const { return durability; }
std::string getBehaviorName() const { return behaviorName; }
private:
std::string name;
int durability = 0;
std::string behaviorName;
};
using std::string;
// Let's assume every function just operates on an Item and returns nothing
using ItemFunc = std::function<void(Item&)>;
// A bunch of functions that define a behavior
using Behavior = std::map<string, ItemFunc, std::less<>>;
// A dictionary of all behaviors
using BehaviorDict = std::map<string, Behavior, std::less<>>;
namespace {
// Simulate boom.chai
std::string const boom_chai = R"(
[
"onDamaged" : fun(Item item) {
var explosionDamage = item.getDurability() * 2;
print("${item.getName()} explodes into a million pieces, dealing ${explosionDamage}");
},
"sayHello" : fun(Item item) {
print("A very special hello from ${item.getName()}!");
}
]
)";
// Simulate darkness.chai
std::string const darkness_chai = R"(
[
"onDamaged" : fun(Item item) {
print("${item.getName()} casts a wave of darkness as it vanishes into thin air!");
}
]
)";
void reloadScripts(chaiscript::ChaiScript& chai, BehaviorDict& behaviors)
{
using namespace chaiscript;
std::vector<std::pair<string, string>> const behaviorScripts = {
{ "boom", boom_chai },
{ "darkness", darkness_chai },
};
for (auto const& [behaviorName, script] : behaviorScripts) {
auto scriptFuncs = chai.eval<std::map<string, Boxed_Value>>(script);
Behavior behavior;
for (auto const& [funcName, func] : scriptFuncs) {
behavior[funcName] = chai.boxed_cast<ItemFunc>(func);
}
if (!behavior.empty()) {
behaviors[behaviorName] = behavior;
}
}
{
// Set up some default behaviors in C++ (if you want)
Behavior defaultBehavior;
defaultBehavior["onDamaged"] = [](Item& item) {
std::cout << item.getName() << " says ouch...\n";
};
defaultBehavior["sayHello"] = [](Item& item) {
std::cout << "Standard hello from " << item.getName() << "...\n";
};
behaviors[""] = defaultBehavior;
}
}
void callBehaviorFunc(Item& item, std::string_view funcName, Behavior const& behavior, Behavior const& defaultBehavior)
{
if (auto it = behavior.find(funcName); it != behavior.end()) {
it->second(item);
}
else {
if (auto defIt = defaultBehavior.find(funcName); defIt != defaultBehavior.end()) {
defIt->second(item);
}
}
}
void evaluate(std::string_view funcName, std::vector<Item>& items, BehaviorDict const& behaviors)
{
auto const& defaultBehavior = behaviors.at("");
for (auto& item : items) {
if (auto behaviorIt = behaviors.find(item.getBehaviorName());
behaviorIt != behaviors.end()) {
callBehaviorFunc(item, funcName, behaviorIt->second, defaultBehavior);
}
}
};
} // unnamed namespace
int main()
{
using namespace chaiscript;
ChaiScript chai;
auto m = std::make_shared<Module>();
utility::add_class<Item>(
*m,
"Item",
{
constructor<Item(string const&, int, string const&)>(),
constructor<Item(Item const&)>(),
},
{
{fun(&Item::getName), "getName"},
{fun(&Item::getDurability), "getDurability"},
});
chai.add(m);
BehaviorDict allBehaviors;
reloadScripts(chai, allBehaviors);
std::vector<Item> allItems;
allItems.emplace_back("hammer of might", 2, "boom");
allItems.emplace_back("wand of magic", 5, "darkness");
allItems.emplace_back("potato of insight", 1, "");
allItems.emplace_back("key of ChaiScript", 3, "boom");
std::cout << "\n===== Calling onDamaged\n";
evaluate("onDamaged", allItems, allBehaviors);
std::cout << "\n===== Calling sayHello\n";
evaluate("sayHello", allItems, allBehaviors);
} Example output: ===== Calling onDamaged
hammer of might explodes into a million pieces, dealing 4
wand of magic casts a wave of darkness as it vanishes into thin air!
potato of insight says ouch...
key of ChaiScript explodes into a million pieces, dealing 6
===== Calling sayHello
A very special hello from hammer of might!
Standard hello from wand of magic...
Standard hello from potato of insight...
A very special hello from key of ChaiScript! A reminder that this is just one way you might approach things...I hope it's helpful! |
Beta Was this translation helpful? Give feedback.
Hi Aaron, what I mentioned in my earlier comment was an idea to return a Map of named functions for each loaded behaviour (or some other way to just return the behaviour data without defining it in global functions). Then you don't need to worry about prefixing things and unique names; instead you store them in a dictionary of named behaviours (each of which has any number of named "callback" functions). You don't store them in variables in the ChaiScript context, and you can redefine them (by loading again and updating the behaviour dictionary) whenever you want.
Here is a moderately simple example of that technique (though complex enough to show a few more possibilities)...it's just one…