Compare commits

..

12 Commits

29 changed files with 415 additions and 294 deletions

1
.gitattributes vendored
View File

@@ -13,6 +13,7 @@
*.jpg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text *.psd filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.sln filter=lfs diff=lfs merge=lfs -text *.sln filter=lfs diff=lfs merge=lfs -text
*.uasset filter=lfs diff=lfs merge=lfs -text *.uasset filter=lfs diff=lfs merge=lfs -text
*.umap filter=lfs diff=lfs merge=lfs -text *.umap filter=lfs diff=lfs merge=lfs -text

View File

@@ -1,17 +0,0 @@
[/Script/UnrealEd.EditorStyleSettings]
AssetEditorOpenLocation=MainWindow
[/Script/BlueprintGraph.BlueprintEditorSettings]
SaveOnCompile=SoC_SuccessOnly
[/Script/UnrealEd.EditorLoadingSavingSettings]
RestoreOpenAssetTabsOnRestart=AlwaysRestore
AutoSaveWarningInSeconds=0
[/Script/Integration.lxProjectSettings]
ActiveServer=/Game/Luprex/KnownServers/SS_Localhost.SS_Localhost

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,6 +2,7 @@
#include "BreakToDebugger.h" #include "BreakToDebugger.h"
#include "Blueprint/BlueprintExceptionInfo.h" #include "Blueprint/BlueprintExceptionInfo.h"
#include "Kismet2/KismetDebugUtilities.h" #include "Kismet2/KismetDebugUtilities.h"
#include "LuprexEditorSettings.h"
ELogVerbosity::Type FlxBreakToDebuggerOutputDevice::ConvertThreshold(ElxBreakToDebuggerThreshold Verbosity) { ELogVerbosity::Type FlxBreakToDebuggerOutputDevice::ConvertThreshold(ElxBreakToDebuggerThreshold Verbosity) {
switch (Verbosity) { switch (Verbosity) {
@@ -15,8 +16,8 @@ ELogVerbosity::Type FlxBreakToDebuggerOutputDevice::ConvertThreshold(ElxBreakToD
} }
} }
FlxBreakToDebuggerOutputDevice::FlxBreakToDebuggerOutputDevice(const ElxBreakToDebuggerThreshold &SensitivityRef) FlxBreakToDebuggerOutputDevice::FlxBreakToDebuggerOutputDevice()
: Sensitivity(SensitivityRef) : Settings(GetDefault<UlxEditorSettings>())
{ {
GLog->AddOutputDevice(this); GLog->AddOutputDevice(this);
} }
@@ -48,7 +49,7 @@ void FlxBreakToDebuggerOutputDevice::Serialize(const TCHAR* V, ELogVerbosity::Ty
{ {
// If the error isn't serious enough, do nothing. // If the error isn't serious enough, do nothing.
// //
if (Verbosity > ConvertThreshold(Sensitivity)) if (Verbosity > ConvertThreshold(Settings->BreakToDebuggerLogVerbosity))
{ {
return; return;
} }
@@ -67,6 +68,13 @@ void FlxBreakToDebuggerOutputDevice::Serialize(const TCHAR* V, ELogVerbosity::Ty
UObject *TopObject = Frame->Object; UObject *TopObject = Frame->Object;
if (TopObject == nullptr) return; if (TopObject == nullptr) return;
// If the category is in the exclude list, do nothing.
//
if (Settings->BreakToDebuggerExcludeCategories.Contains(Category))
{
return;
}
// Notify the debugger that there's been an exception. // Notify the debugger that there's been an exception.
// //
FBlueprintExceptionInfo ExceptionInfo(EBlueprintExceptionType::Breakpoint, FText::FromStringView(FStringView(V))); FBlueprintExceptionInfo ExceptionInfo(EBlueprintExceptionType::Breakpoint, FText::FromStringView(FStringView(V)));

View File

@@ -79,19 +79,15 @@ enum class ElxBreakToDebuggerThreshold : uint8 {
}; };
class UlxEditorSettings;
struct FlxBreakToDebuggerOutputDevice : public FOutputDevice struct FlxBreakToDebuggerOutputDevice : public FOutputDevice
{ {
public: public:
// The constructor and destructor automatically register // The constructor and destructor automatically register
// this output device with GLog. // this output device with GLog.
// //
// This struct doesn't store the sensitivity threshold. FlxBreakToDebuggerOutputDevice();
// It relies on the LuprexGameMode class to do that, so
// that the threshold can be easily edited with the
// blueprint editor. This struct must be initialized
// with a reference to the threshold variable.
//
FlxBreakToDebuggerOutputDevice(const ElxBreakToDebuggerThreshold &SensitivityRef);
~FlxBreakToDebuggerOutputDevice(); ~FlxBreakToDebuggerOutputDevice();
// Inspect a log message. // Inspect a log message.
@@ -109,5 +105,5 @@ public:
private: private:
static ELogVerbosity::Type ConvertThreshold(ElxBreakToDebuggerThreshold Verbosity); static ELogVerbosity::Type ConvertThreshold(ElxBreakToDebuggerThreshold Verbosity);
const ElxBreakToDebuggerThreshold &Sensitivity; const UlxEditorSettings *Settings = nullptr;
}; };

View File

@@ -2,3 +2,4 @@
DEFINE_LOG_CATEGORY(LogLuprex); DEFINE_LOG_CATEGORY(LogLuprex);
DEFINE_LOG_CATEGORY(LogLuprexIntegration); DEFINE_LOG_CATEGORY(LogLuprexIntegration);
DEFINE_LOG_CATEGORY(LogLuprexConsole);

View File

@@ -153,3 +153,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogLuprex, Display, All);
// Messages about the Luprex integration with Unreal. // Messages about the Luprex integration with Unreal.
// //
DECLARE_LOG_CATEGORY_EXTERN(LogLuprexIntegration, Display, All); DECLARE_LOG_CATEGORY_EXTERN(LogLuprexIntegration, Display, All);
// Messages from the Luprex console.
//
DECLARE_LOG_CATEGORY_EXTERN(LogLuprexConsole, Display, All);

View File

@@ -0,0 +1,7 @@
#include "LuprexEditorSettings.h"
#include "Common.h"
UlxEditorSettings::UlxEditorSettings()
{
BreakToDebuggerExcludeCategories.Add(LogLuprexConsole.GetCategoryName());
}

View File

@@ -3,7 +3,8 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Engine/DataAsset.h" #include "Engine/DataAsset.h"
#include "Engine/DeveloperSettings.h" #include "Engine/DeveloperSettings.h"
#include "ProjectSettings.generated.h" #include "BreakToDebugger.h"
#include "LuprexEditorSettings.generated.h"
UCLASS(BlueprintType) UCLASS(BlueprintType)
class INTEGRATION_API UlxServerSelection : public UPrimaryDataAsset class INTEGRATION_API UlxServerSelection : public UPrimaryDataAsset
@@ -18,17 +19,31 @@ public:
FString Host; FString Host;
}; };
UCLASS(Config=EditorPerProjectUserSettings, DefaultConfig, meta=(DisplayName="Server Settings")) UCLASS(Config=EditorPerProjectUserSettings, DefaultConfig, meta=(DisplayName="Luprex Editor Settings"))
class INTEGRATION_API UlxProjectSettings : public UDeveloperSettings class INTEGRATION_API UlxEditorSettings : public UDeveloperSettings
{ {
GENERATED_BODY() GENERATED_BODY()
public: public:
UlxEditorSettings();
virtual FName GetContainerName() const override { return TEXT("Editor"); } virtual FName GetContainerName() const override { return TEXT("Editor"); }
virtual FName GetCategoryName() const override { return TEXT("Luprex"); } virtual FName GetCategoryName() const override { return TEXT("Luprex"); }
virtual FName GetSectionName() const override { return TEXT("Server"); } virtual FName GetSectionName() const override { return TEXT("Settings"); }
// The server to connect to. You can add an option to the list by creating
// a new data asset of type 'lx Server Selection'.
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category="Server", UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category="Server",
meta=(AllowedClasses="/Script/Integration.lxServerSelection")) meta=(AllowedClasses="/Script/Integration.lxServerSelection"))
TSoftObjectPtr<UlxServerSelection> ActiveServer; TSoftObjectPtr<UlxServerSelection> ActiveServer;
// If blueprint execution generates a log message of at least the following
// severity, the blueprint debugger will automatically pause with a breakpoint.
UPROPERTY(Config, EditAnywhere, Category="Debugging Tools")
ElxBreakToDebuggerThreshold BreakToDebuggerLogVerbosity;
// Log categories in this set are excluded from BreakToDebugger triggering.
UPROPERTY(Config, EditAnywhere, Category="Debugging Tools")
TSet<FName> BreakToDebuggerExcludeCategories;
}; };

View File

@@ -12,7 +12,7 @@
#include "Blueprint/WidgetBlueprintLibrary.h" #include "Blueprint/WidgetBlueprintLibrary.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h" #include "Engine/GameInstance.h"
#include "ProjectSettings.h" #include "LuprexEditorSettings.h"
#include "Common.h" #include "Common.h"
#include "AnimQueue.h" #include "AnimQueue.h"
@@ -215,7 +215,7 @@ void ALuprexGameModeBase::InitializeGlobalState()
// Possibly tell the engine to connect to a server. // Possibly tell the engine to connect to a server.
if (Playing) { if (Playing) {
UlxServerSelection* Server = GetDefault<UlxProjectSettings>()->ActiveServer.LoadSynchronous(); UlxServerSelection* Server = GetDefault<UlxEditorSettings>()->ActiveServer.LoadSynchronous();
if (Server) { if (Server) {
FString Host = Server->Host.TrimStartAndEnd(); FString Host = Server->Host.TrimStartAndEnd();
if (!Host.IsEmpty()) { if (!Host.IsEmpty()) {
@@ -238,7 +238,7 @@ void ALuprexGameModeBase::InitializeGlobalState()
// If somebody generates a log message that's severe enough, break to debugger. // If somebody generates a log message that's severe enough, break to debugger.
BreakToDebuggerLogVerbosityDevice.Reset( BreakToDebuggerLogVerbosityDevice.Reset(
new FlxBreakToDebuggerOutputDevice(BreakToDebuggerLogVerbosity)); new FlxBreakToDebuggerOutputDevice());
} }
void ALuprexGameModeBase::EndPlay(const EEndPlayReason::Type EndPlayReason) void ALuprexGameModeBase::EndPlay(const EEndPlayReason::Type EndPlayReason)

View File

@@ -68,10 +68,6 @@ public:
UFUNCTION(BlueprintCallable, Category = "Luprex|Miscellaneous", meta = (WorldContext = "WorldContextObject")) UFUNCTION(BlueprintCallable, Category = "Luprex|Miscellaneous", meta = (WorldContext = "WorldContextObject"))
static void TriggerReloadSource(const UObject *WorldContextObject); static void TriggerReloadSource(const UObject *WorldContextObject);
// The sensitivity level at which a log message triggers a debugger breakpoint.
UPROPERTY(EditAnywhere, Category="Debugging Tools")
ElxBreakToDebuggerThreshold BreakToDebuggerLogVerbosity;
// Luprex socket system. // Luprex socket system.
TUniquePtr<FlxSockets> Sockets; TUniquePtr<FlxSockets> Sockets;

View File

@@ -1 +0,0 @@
#include "ProjectSettings.h"

View File

@@ -242,3 +242,8 @@ void UlxUtilityLibrary::ValidateLuaExpr(
Status = w.ValidateLuaExpr(Code, ErrorMessage); Status = w.ValidateLuaExpr(Code, ErrorMessage);
} }
void UlxUtilityLibrary::LogLuprexConsoleMessage(const FString &Message)
{
UE_LOG(LogLuprexConsole, Warning, TEXT("%s"), *Message);
}

View File

@@ -144,4 +144,9 @@ public:
// //
UFUNCTION(BlueprintCallable, meta = (WorldContext = "context"), Category = "Luprex|Utility") UFUNCTION(BlueprintCallable, meta = (WorldContext = "context"), Category = "Luprex|Utility")
static void ValidateLuaExpr(ElxLuaSyntaxCheck &Status, FString &ErrorMessage, UObject *context, const FString &Code); static void ValidateLuaExpr(ElxLuaSyntaxCheck &Status, FString &ErrorMessage, UObject *context, const FString &Code);
// Log a message to the LogLuprexConsole category.
//
UFUNCTION(BlueprintCallable, Category = "Luprex|Utility")
static void LogLuprexConsoleMessage(const FString &Message);
}; };

4
TeppyNotes.txt Normal file
View File

@@ -0,0 +1,4 @@
- To run a server:
- cd luprex
- ./build/Linux/luprexstatic lpxserver

View File

@@ -18,6 +18,8 @@ public:
PrintChanneler print_channeler_; PrintChanneler print_channeler_;
eng::vector<Invocation> delayed_invocations_; eng::vector<Invocation> delayed_invocations_;
lua_State *lua_syntax_checker_; lua_State *lua_syntax_checker_;
eng::string auto_reconnect_target_;
double auto_reconnect_time_ = 0.0;
public: public:
LpxClient(EngineWrapper *w) : wrapper_(w) { LpxClient(EngineWrapper *w) : wrapper_(w) {
@@ -58,6 +60,10 @@ public:
// Clear any saved invocations // Clear any saved invocations
delayed_invocations_.clear(); delayed_invocations_.clear();
// Clear auto-reconnect state.
auto_reconnect_target_.clear();
auto_reconnect_time_ = 0.0;
} }
void set_initial_state_standalone() { void set_initial_state_standalone() {
@@ -83,6 +89,10 @@ public:
// Clear any saved invocations // Clear any saved invocations
delayed_invocations_.clear(); delayed_invocations_.clear();
// Clear auto-reconnect state.
auto_reconnect_target_.clear();
auto_reconnect_time_ = 0.0;
} }
// When the world is in synchronous mode, there's no // When the world is in synchronous mode, there's no
@@ -108,10 +118,12 @@ public:
} }
} }
void abandon_server() { void disconnect_and_reconnect() {
if (channel_) if (channel_)
{ {
set_initial_state_standalone(); auto_reconnect_target_ = channel_->target();
auto_reconnect_time_ = get_clock() + 5.0;
channel_.reset();
} }
} }
@@ -138,7 +150,7 @@ public:
// An ack is just a single byte, so there's nothing left to read. // An ack is just a single byte, so there's nothing left to read.
if (unack_.empty()) { if (unack_.empty()) {
// Invalid acknowledgement when theres' nothing in the unack queue. // Invalid acknowledgement when theres' nothing in the unack queue.
abandon_server(); disconnect_and_reconnect();
return; return;
} }
world_to_synchronous(); world_to_synchronous();
@@ -158,7 +170,7 @@ public:
} }
// dbc.dump(...); // dbc.dump(...);
} catch (const StreamException &sexcept) { } catch (const StreamException &sexcept) {
abandon_server(); disconnect_and_reconnect();
return; return;
} }
} }
@@ -179,11 +191,11 @@ public:
} else if (message_type == util::MSG_DIFF) { } else if (message_type == util::MSG_DIFF) {
receive_diff_from_server(&body); receive_diff_from_server(&body);
} else { } else {
abandon_server(); disconnect_and_reconnect();
return false; return false;
} }
if (!body.empty()) { if (!body.empty()) {
abandon_server(); disconnect_and_reconnect();
return false; return false;
} }
return true; return true;
@@ -248,7 +260,7 @@ public:
if (channel_ != nullptr) { if (channel_ != nullptr) {
if (channel_->closed()) { if (channel_->closed()) {
util::dprint("server closed connection: ", channel_->error()); util::dprint("server closed connection: ", channel_->error());
abandon_server(); disconnect_and_reconnect();
} else { } else {
while (true) { while (true) {
if (!receive_message_from_server(channel_->in())) break; if (!receive_message_from_server(channel_->in())) break;
@@ -256,6 +268,10 @@ public:
} }
world_to_asynchronous(); world_to_asynchronous();
} }
} else {
if ((!auto_reconnect_target_.empty()) && (get_clock() > auto_reconnect_time_)) {
set_initial_state_connect(auto_reconnect_target_);
}
} }
set_have_prints(print_channeler_.have_prints(world_->get_printbuffer(actor_id_))); set_have_prints(print_channeler_.have_prints(world_->get_printbuffer(actor_id_)));

View File

@@ -77,6 +77,7 @@ lua_State *LuaCoreStack::newstate (lua_Alloc allocf) {
LS.rawset(LuaRegistry, "persist", LuaNewTable); LS.rawset(LuaRegistry, "persist", LuaNewTable);
LS.rawset(LuaRegistry, "unpersist", LuaNewTable); LS.rawset(LuaRegistry, "unpersist", LuaNewTable);
LS.rawset(LuaRegistry, "funcnames", LuaNewTable); LS.rawset(LuaRegistry, "funcnames", LuaNewTable);
LS.rawset(LuaRegistry, "sourcedb", LuaNewTable);
// Tag the registry and global environment with their tabletypes. // Tag the registry and global environment with their tabletypes.
LS.settabletype(LuaRegistry, LUA_TT_REGISTRY); LS.settabletype(LuaRegistry, LUA_TT_REGISTRY);

View File

@@ -171,6 +171,7 @@ public:
LuaExtraArgs(int i, int s) : index_(i), size_(s) {} LuaExtraArgs(int i, int s) : index_(i), size_(s) {}
LuaSpecial operator[] (int n) const { return LuaSpecial(index_ + n); } LuaSpecial operator[] (int n) const { return LuaSpecial(index_ + n); }
int index(int n) const { return index_ + n; }
int size() const { return size_; } int size() const { return size_; }
friend class LuaCoreStack; friend class LuaCoreStack;

View File

@@ -132,16 +132,6 @@ static void get_info_table(LuaCoreStack &LS, LuaSlot db, LuaSlot info, const eng
LS.rawset(info, "name", fn); LS.rawset(info, "name", fn);
} }
static void calculate_loadresult(LuaCoreStack &LS0, LuaSlot info, const eng::string &fn, const eng::string &code) {
LuaVar loadresult;
LuaExtStack LS(LS0.state(), loadresult);
if (code == "") {
LS.rawset(info, "loadresult", "missing or empty source file");
} else {
LS.load(loadresult, code, fn);
LS.rawset(info, "loadresult", loadresult);
}
}
void SourceDB::diff(const SourceDB &auth, StreamBuffer *sb) { void SourceDB::diff(const SourceDB &auth, StreamBuffer *sb) {
LuaVar sdb, sfn, sinfo, shash, sseq; LuaVar sdb, sfn, sinfo, shash, sseq;
@@ -202,8 +192,8 @@ void SourceDB::diff(const SourceDB &auth, StreamBuffer *sb) {
bool SourceDB::patch(StreamBuffer *sb, DebugCollector *dbc) { bool SourceDB::patch(StreamBuffer *sb, DebugCollector *dbc) {
lua_State *L = lua_state_; lua_State *L = lua_state_;
LuaVar db, info; LuaVar db, info, closure;
LuaExtStack LS(L, db, info); LuaExtStack LS(L, db, info, closure);
LS.rawget(db, LuaRegistry, "sourcedb"); LS.rawget(db, LuaRegistry, "sourcedb");
int nupdates = sb->read_int32(); int nupdates = sb->read_int32();
for (int i = 0; i < nupdates; i++) { for (int i = 0; i < nupdates; i++) {
@@ -219,24 +209,27 @@ bool SourceDB::patch(StreamBuffer *sb, DebugCollector *dbc) {
if (code != "\001") { if (code != "\001") {
LS.rawset(info, "code", code); LS.rawset(info, "code", code);
LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code))); LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code)));
calculate_loadresult(LS, info, fn, code); LS.load(closure, code, fn);
LS.rawset(info, "loadresult", closure);
} }
} }
} }
if (nupdates > 0) rebuild();
return (nupdates > 0); return (nupdates > 0);
} }
void SourceDB::set(const eng::string &fn, const eng::string &code, int sequence) { void SourceDB::set(const eng::string &fn, const eng::string &code, int sequence) {
lua_State *L = lua_state_; lua_State *L = lua_state_;
LuaVar db, info; LuaVar db, info, closure;
LuaExtStack LS(L, db, info); LuaExtStack LS(L, db, info, closure);
LS.rawget(db, LuaRegistry, "sourcedb"); LS.rawget(db, LuaRegistry, "sourcedb");
get_info_table(LS, db, info, fn); get_info_table(LS, db, info, fn);
LS.rawset(info, "sequence", sequence); LS.rawset(info, "sequence", sequence);
LS.rawset(info, "code", code); LS.rawset(info, "code", code);
LS.rawset(info, "fingerprint", ""); LS.rawset(info, "fingerprint", "");
LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code))); LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code)));
calculate_loadresult(LS, info, fn, code); LS.load(closure, code, fn);
LS.rawset(info, "loadresult", closure);
} }
eng::string SourceDB::get(const eng::string &fn) { eng::string SourceDB::get(const eng::string &fn) {
@@ -262,19 +255,15 @@ eng::string SourceDB::get(const eng::string &fn) {
return util::ss(seqno, ":", ccode, ":", cloadresult); return util::ss(seqno, ":", ccode, ":", cloadresult);
} }
void SourceDB::update(const util::LuaSourceVec &source) { eng::string SourceDB::update(const util::LuaSourceVec &source) {
lua_State *L = lua_state_; lua_State *L = lua_state_;
LuaVar sourcedb, info; LuaVar newsourcedb, oldsourcedb, info, closure;
LuaExtStack LS(L, sourcedb, info); LuaExtStack LS(L, newsourcedb, oldsourcedb, info, closure);
// Get and clear the source database. eng::ostringstream errors;
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
if (!LS.istable(sourcedb)) {
LS.newtable(sourcedb);
LS.rawset(LuaRegistry, "sourcedb", sourcedb);
}
LS.cleartable(sourcedb, true);
// Step one. Build the new sourcedb.
LS.newtable(newsourcedb);
for (int i = 0; i < int(source.size()); i++) { for (int i = 0; i < int(source.size()); i++) {
const eng::string &file = source[i].first; const eng::string &file = source[i].first;
const eng::string &code = source[i].second; const eng::string &code = source[i].second;
@@ -283,9 +272,32 @@ void SourceDB::update(const util::LuaSourceVec &source) {
LS.rawset(info, "code", code); LS.rawset(info, "code", code);
LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code))); LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code)));
LS.rawset(info, "sequence", i + 1); LS.rawset(info, "sequence", i + 1);
calculate_loadresult(LS, info, file, code); LS.load(closure, code, file);
LS.rawset(sourcedb, file, info); LS.rawset(info, "loadresult", closure);
if (LS.isstring(closure)) {
errors << file << ":" << LS.ckstring(closure) << std::endl;
}
LS.rawset(newsourcedb, file, info);
} }
// If there were compile errors, return the errors and don't update the env.
if (!errors.view().empty()) {
return errors.str();
}
// Cache the old source database and store the new one.
LS.rawget(oldsourcedb, LuaRegistry, "sourcedb");
LS.rawset(LuaRegistry, "sourcedb", newsourcedb);
// Rebuild the source database.
eng::string rebuild_errors = rebuild();
// Restore the old state, if there were errors.
if (!rebuild_errors.empty()) {
LS.rawset(LuaRegistry, "sourcedb", oldsourcedb);
rebuild();
}
return rebuild_errors;
} }
// Delete everything from the global environment. // Delete everything from the global environment.
@@ -411,14 +423,11 @@ eng::string SourceDB::get_source(const eng::string &fn)
return LS.ckstring(code); return LS.ckstring(code);
} }
eng::vector<eng::string> SourceDB::modules() { eng::vector<eng::string> SourceDB::modules() {
eng::vector<eng::string> result; eng::vector<eng::string> result;
LuaVar sourcedb, key, info, seq; LuaVar sourcedb, key, info, seq;
LuaExtStack LS(lua_state_, sourcedb, key, info, seq); LuaExtStack LS(lua_state_, sourcedb, key, info, seq);
result.push_back("CORE");
// Get the source database. // Get the source database.
LS.rawget(sourcedb, LuaRegistry, "sourcedb"); LS.rawget(sourcedb, LuaRegistry, "sourcedb");
if (!LS.istable(sourcedb)) { if (!LS.istable(sourcedb)) {
@@ -439,35 +448,38 @@ eng::vector<eng::string> SourceDB::modules() {
return result; return result;
} }
void SourceDB::rebuild_core() { eng::string SourceDB::rebuild_module(const eng::string &mod) {
LuaVar sourcedb, info, closure;
LuaExtStack LS(lua_state_, sourcedb, info, closure);
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
LS.rawget(info, sourcedb, mod);
if (!LS.istable(info)) {
return util::ss("No such module: ", mod);
}
LS.rawget(closure, info, "loadresult");
if (!LS.isfunction(closure)) {
return util::ss(mod, ":", LS.ckstring(closure));
}
lua_pushvalue(lua_state_, closure.index());
return traceback_pcall(lua_state_, 0, 0);
}
eng::string SourceDB::rebuild() {
source_clear_globals(lua_state_); source_clear_globals(lua_state_);
source_load_cfunctions(lua_state_); source_load_cfunctions(lua_state_);
source_load_cconstants(lua_state_); source_load_cconstants(lua_state_);
eng::ostringstream errors;
for (const eng::string &mod: modules()) {
eng::string err = rebuild_module(mod);
if (!err.empty()) errors << err << std::endl;
}
rebuild_funcnames();
return errors.str();
} }
eng::string SourceDB::rebuild_module(const eng::string &mod) {
if (mod == "CORE") {
rebuild_core();
return "";
} else {
LuaVar sourcedb, info, closure;
LuaExtStack LS(lua_state_, sourcedb, info, closure);
LS.rawget(sourcedb, LuaRegistry, "sourcedb");
if (!LS.istable(sourcedb)) {
return "SourceDB not initialized";
}
LS.rawget(info, sourcedb, mod);
if (!LS.istable(info)) {
return util::ss("No such module: ", mod);
}
LS.rawget(closure, info, "loadresult");
if (!LS.isfunction(closure)) {
return util::ss(mod, ":", LS.ckstring(closure));
}
lua_pushvalue(lua_state_, closure.index());
return traceback_pcall(lua_state_, 0, 0);
}
}
void SourceDB::init(lua_State *L) { void SourceDB::init(lua_State *L) {
register_lua_builtins(); register_lua_builtins();
@@ -482,8 +494,7 @@ void SourceDB::init(lua_State *L) {
LS.set(nullstring, ""); LS.set(nullstring, "");
LS.setmetatable(nullstring, classtab); LS.setmetatable(nullstring, classtab);
// Rebuild the global environment. rebuild();
rebuild_core();
// We need to register all C functions with the eris permanents tables. // We need to register all C functions with the eris permanents tables.
LS.rawget(persist, LuaRegistry, "persist"); LS.rawget(persist, LuaRegistry, "persist");
@@ -516,15 +527,22 @@ void SourceDB::serialize_source(const util::LuaSourceVec &sv, StreamBuffer *sb)
} }
} }
void SourceDB::deserialize_source(util::LuaSourceVec *sv, StreamBuffer *sb) { util::LuaSourceVec SourceDB::deserialize_source(std::string_view datapack) {
sv->clear(); StreamBuffer sb(datapack);
int count = sb->read_int32(); util::LuaSourceVec sv;
if ((count < 0) || (count > 10000)) throw StreamCorruption(); try {
for (int i = 0; i < count; i++) { sv.clear();
eng::string fn = sb->read_string(); int count = sb.read_int32();
eng::string code = sb->read_string(); if ((count < 0) || (count > 10000)) throw StreamCorruption();
sv->emplace_back(fn, code); for (int i = 0; i < count; i++) {
eng::string fn = sb.read_string();
eng::string code = sb.read_string();
sv.emplace_back(fn, code);
}
} catch (const StreamException &ex) {
sv.clear();
} }
return sv;
} }
// Register lua builtins. // Register lua builtins.
@@ -630,7 +648,6 @@ bool SourceDB::search_docs(const eng::string &substring, std::ostream &ostream)
// Search the lua source code. // Search the lua source code.
for (const eng::string &module : modules()) { for (const eng::string &module : modules()) {
if (module == "CORE") continue;
eng::string code = get_source(module); eng::string code = get_source(module);
if (code.empty()) continue; if (code.empty()) continue;
util::StringVec lines = util::split_lines(code); util::StringVec lines = util::split_lines(code);

View File

@@ -132,58 +132,31 @@ private:
public: public:
void init(lua_State *L); void init(lua_State *L);
// Update // modules()
// //
// Update the database using the specified lua source code. // Returns a list of all the lua modules, in the proper order.
// Compiles these files using lua's "load" function.
//
void update(const util::LuaSourceVec &source);
// modules
//
// Returns a list of all the modules. The first item in the list
// is always the string "CORE" which represents the lua core
// functionality with all the builtins. This is then followed by
// all the lua sourcefiles in the correct order.
// //
eng::vector<eng::string> modules(); eng::vector<eng::string> modules();
// Update
// get_source
// //
// Get the source code for a given module. // Try to compile and load the specified source. Then, rebuild the
// global environment.
// //
eng::string get_source(const eng::string &fn); // If this generates any errors, puts back the old code, and rebuilds
// the global environment using the old code.
// rebuild_module
// //
// To rebuild the lua environment, fetch the module list, then // Returns any error messages. If this returns empty string, it means
// call rebuild_module on each module in turn. This will return // there were no errors and the code was successfully update. If there
// an error message for the module, or empty string if no error. // are any error messages, it means we restored the old code as best as
// possible.
// //
// This is a thin wrapper around traceback_pcall. The return eng::string update(const util::LuaSourceVec &source);
// value is the return value of traceback_pcall.
//
eng::string rebuild_module(const eng::string &mod);
// rebuild_core
//
// This is equivalent to rebuild_module("CORE"). Clears the environment
// and installs all the builtins. No error conditions.
//
void rebuild_core();
// rebuild_funcnames
//
// Traverses the global environment and populates the registry "funcnames"
// table, mapping each closure to its name.
//
void rebuild_funcnames();
// Difference transmission. // Difference transmission.
// //
// Note: The patch routine applies the differences to the source // Note: The patch routine applies the differences to the source
// database, and if there are any changes, it does a source rebuild. // database, and if there are any changes, it does a source rebuild.
// The patch routine returns true if anything was modified. //
// //
void diff(const SourceDB &auth, StreamBuffer *sb); void diff(const SourceDB &auth, StreamBuffer *sb);
bool patch(StreamBuffer *sb, DebugCollector *dbc); bool patch(StreamBuffer *sb, DebugCollector *dbc);
@@ -213,7 +186,54 @@ public:
// Serialize and unserialize a source vector. // Serialize and unserialize a source vector.
// //
static void serialize_source(const util::LuaSourceVec &sv, StreamBuffer *sb); static void serialize_source(const util::LuaSourceVec &sv, StreamBuffer *sb);
static void deserialize_source(util::LuaSourceVec *sv, StreamBuffer *sb); static util::LuaSourceVec deserialize_source(std::string_view datapack);
private:
//////////////////////////////////////////////////////////////////////////
//
// Internal implementation stuff.
//
//////////////////////////////////////////////////////////////////////////
// rebuild
//
// Rebuild the global environment from the sourcedb:
//
// * Clears the environment
// * Installs the builtins
// * Executes all the closures in the sourcedb.
// * Regenerates the function-names table.
//
// The closures may generate errors, if so, this returns the error
// messages. Note that if there are errors, there is no automatic cleanup.
//
eng::string rebuild();
// get_source
//
// Get the source code for a given module.
//
eng::string get_source(const eng::string &fn);
// rebuild_module
//
// To rebuild the lua environment, fetch the module list, then
// call rebuild_module on each module in turn. This will return
// an error message for the module, or empty string if no error.
//
// This is a thin wrapper around traceback_pcall. The return
// value is the return value of traceback_pcall.
//
eng::string rebuild_module(const eng::string &mod);
// rebuild_funcnames
//
// Traverses the global environment and populates the registry "funcnames"
// table, mapping each closure to its name.
//
void rebuild_funcnames();
}; };

View File

@@ -55,7 +55,8 @@ public:
virtual void event_access(AccessKind kind, int64_t place_id, std::string_view datapk, StreamBuffer *retpk) override { virtual void event_access(AccessKind kind, int64_t place_id, std::string_view datapk, StreamBuffer *retpk) override {
switch (kind) { switch (kind) {
case AccessKind::INVOKE_LUA_SOURCE: { case AccessKind::INVOKE_LUA_SOURCE: {
world_->update_source(datapk, 0); Invocation inv(kind, place_id, place_id, datapk);
world_->invoke(0, inv);
run_unittests(world_->state()); run_unittests(world_->state());
stop_driver(); stop_driver();
break; break;

View File

@@ -607,20 +607,13 @@ LuaDefine(tangible_start, "tangible,function,arg1,arg2...",
"|to continue running until it blocks. But from that point" "|to continue running until it blocks. But from that point"
"|forward, the thread will never be resumed for any reason.") { "|forward, the thread will never be resumed for any reason.") {
int top = lua_gettop(L);
if (top < 2) {
luaL_error(L, "Not enough arguments to tangible.start");
return 0;
}
int varlen = top - 2;
World *w = World::fetch_global_pointer(L); World *w = World::fetch_global_pointer(L);
w->guard_blockable(L, "tangible.start"); w->guard_blockable(L, "tangible.start");
LuaArg place, fname;
LuaExtraArgs extra;
LuaVar mt, classtab, plthreads, thread, thinfo, func, tanlist; LuaVar mt, classtab, plthreads, thread, thinfo, func, tanlist;
LuaDefStack LS(L, mt, classtab, plthreads, thread, thinfo, func, tanlist); LuaDefStack LS(L, place, fname, extra, mt, classtab, plthreads, thread, thinfo, func, tanlist);
LuaSpecial place(1);
LuaSpecial fname(2);
// If they passed in a single tangible, convert it to a tangible list. // If they passed in a single tangible, convert it to a tangible list.
int64_t place_id = LS.tanid(place); int64_t place_id = LS.tanid(place);
@@ -677,10 +670,10 @@ LuaDefine(tangible_start, "tangible,function,arg1,arg2...",
// Create a new thread, set up function and arguments. // Create a new thread, set up function and arguments.
lua_State *CO = LS.newthread(thread); lua_State *CO = LS.newthread(thread);
lua_pushvalue(L, func.index()); lua_pushvalue(L, func.index());
for (int i = 0; i < varlen; i++) { for (int i = 0; i < extra.size(); i++) {
lua_pushvalue(L, i + 3); lua_pushvalue(L, extra.index(i));
} }
lua_xmove(L, CO, varlen + 1); lua_xmove(L, CO, extra.size() + 1);
// Create the thread info table. // Create the thread info table.
LS.newtable(thinfo); LS.newtable(thinfo);

View File

@@ -310,10 +310,13 @@ int64_t World::connection_create() {
assert(stack_is_clear()); assert(stack_is_clear());
int64_t id = id_global_pool_.get_one(); int64_t id = id_global_pool_.get_one();
{ {
LuaVar database, classtab, mt, func; LuaVar database, classtab, config, mt, func;
LuaExtStack LS(state(), database, classtab, mt, func); LuaExtStack LS(state(), database, classtab, config, mt, func);
Tangible *tan = tangible_make(LS, database, id); Tangible *tan = tangible_make(LS, database, id);
// A blank config table to pass to 'init'
LS.newtable(config);
// Set the login flags. // Set the login flags.
if (is_authoritative()) { if (is_authoritative()) {
tan->client_id_ = id; tan->client_id_ = id;
@@ -327,7 +330,9 @@ int64_t World::connection_create() {
if (is_authoritative()) { if (is_authoritative()) {
LS.rawget(func, classtab, "init"); LS.rawget(func, classtab, "init");
spawn(LS, id, id, func, 0, false); lua_pushvalue(state(), database.index());
lua_pushvalue(state(), config.index());
spawn(LS, id, id, func, 2, false);
} }
} }
connections_.emplace(id, id); connections_.emplace(id, id);
@@ -572,71 +577,6 @@ void World::probe_lua_call(int64_t actor_id, int64_t place_id, std::string_view
clear_lthread_state(); clear_lthread_state();
} }
// This is called from World::update_source, and also
// from World::patch_source in the difference transmitter.
//
// When called from the difference transmitter, we suppress
// error messages.
//
// For the moment, errors are channeled to util::dprint,
// and 'print' statements just go to std::cerr. Neither
// of these is ideal. We need to get serious about setting
// up error handling.
//
// We also need to figure out a solution for what happens if
// some lua source file tries to modify, say, tangible state
// in top-level code.
//
bool World::rebuild_sourcedb(int64_t actor_id) {
int successes = 0;
int failures = 0;
for (const eng::string &mod: source_db_.modules()) {
open_lthread_state(0, 0, 0, false);
eng::string err = source_db_.rebuild_module(mod);
eng::string prints = lthread_prints_.str();
clear_lthread_state();
if (err.empty()) successes ++;
else failures ++;
if ((!err.empty()) || (!prints.empty())) {
lthread_prints_ << "Compiling " << mod << ":" << std::endl;
if (!err.empty()) lthread_prints_ << err << std::endl;
if (!prints.empty()) lthread_prints_ << prints;
util::dprintview(lthread_prints_.view());
if (actor_id != 0) lthread_prints_to_actor(actor_id);
}
}
source_db_.rebuild_funcnames();
lthread_prints_ << "Compiled " << successes << " modules successfully." << std::endl;
if (failures > 0) {
lthread_prints_ << "Compiled " << failures << " modules with errors." << std::endl;
}
util::dprintview(lthread_prints_.view());
if (actor_id > 0) lthread_prints_to_actor(actor_id);
return (failures == 0);
}
bool World::update_source(const util::LuaSourceVec &source, int64_t actor_id) {
assert(stack_is_clear());
source_db_.update(source);
return rebuild_sourcedb(actor_id);
assert(stack_is_clear());
}
bool World::update_source(std::string_view sourcepack, int64_t actor_id) {
if (sourcepack.empty()) {
return false;
}
try {
StreamBuffer sb(sourcepack);
util::LuaSourceVec sv;
SourceDB::deserialize_source(&sv, &sb);
return update_source(sv, actor_id);
} catch (const StreamException &ex) {
return false;
}
}
void World::http_response(const HttpParser &response) { void World::http_response(const HttpParser &response) {
// Find the request. // Find the request.
auto iter = http_requests_.find(response.request_id()); auto iter = http_requests_.find(response.request_id());
@@ -1019,23 +959,41 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_vi
if (actor_id != place_id) return; if (actor_id != place_id) return;
// Check if this is the first time we're loading the source. // Check if this is the first time we're loading the source.
bool brand_new = (source_db_.modules().size() == 1); bool brand_new = (source_db_.modules().size() == 0);
// Compile and load the source. // Deserialize the datapack.
bool success = update_source(datapack, actor_id); util::LuaSourceVec sv = SourceDB::deserialize_source(datapack);
if (sv.empty()) return;
// Try to compile the code.
open_lthread_state(actor_id, actor_id, 0, false);
eng::string errors = source_db_.update(sv);
if (errors.empty()) {
lthread_prints_ << "Compiled source successfully.\n";
} else {
lthread_prints_ << "Compiling source: \n" << errors;
}
util::dprint(lthread_prints_.view());
lthread_prints_to_actor(actor_id);
// Call world.init // Call world.init
if (brand_new) { if (brand_new) {
if (success) { if (errors.empty()) {
{ {
lua_State *L = state(); lua_State *L = state();
LuaVar lclass, lfunc; LuaVar lclass, lfunc, tangibles, actor, config;
LuaExtStack LS(L, lclass, lfunc); LuaExtStack LS(L, lclass, lfunc, tangibles, actor, config);
LS.rawget(tangibles, LuaRegistry, "tangibles");
LS.rawget(actor, tangibles, actor_id);
LS.newtable(config);
LS.getclass(lclass, "world"); LS.getclass(lclass, "world");
if (LS.classname(lclass) != "") { if (LS.classname(lclass) != "") {
LS.rawget(lfunc, lclass, "init"); LS.rawget(lfunc, lclass, "init");
spawn(LS, actor_id, place_id, lfunc, 0, false); lua_pushvalue(L, actor.index());
lua_pushvalue(L, config.index());
spawn(LS, actor_id, place_id, lfunc, 2, false);
} }
} }
run_scheduled_threads(); run_scheduled_threads();

View File

@@ -290,11 +290,7 @@ void World::diff_tanclass(int64_t actor_id, World *master, StreamBuffer *xsb) {
void World::patch_source(StreamBuffer *sb, DebugCollector *dbc) { void World::patch_source(StreamBuffer *sb, DebugCollector *dbc) {
DebugBlock dbb(dbc, "patch_source"); DebugBlock dbb(dbc, "patch_source");
bool modified = source_db_.patch(sb, dbc); source_db_.patch(sb, dbc);
if (modified) {
rebuild_sourcedb(0);
DebugLine(dbc) << "Source DB rebuilt";
}
} }
void World::diff_source(World *master, StreamBuffer *sb) { void World::diff_source(World *master, StreamBuffer *sb) {
@@ -359,6 +355,7 @@ void World::diff_globals(World *master, StreamBuffer *sb) {
int64_t World::patch(StreamBuffer *sb, DebugCollector *dbc) { int64_t World::patch(StreamBuffer *sb, DebugCollector *dbc) {
DebugBlock dbb(dbc, "patch"); DebugBlock dbb(dbc, "patch");
clear_lthread_state();
int64_t actor_id = patch_actor(sb, dbc); int64_t actor_id = patch_actor(sb, dbc);
patch_visible(sb, dbc); patch_visible(sb, dbc);
bool full = sb->read_bool(); bool full = sb->read_bool();

View File

@@ -242,23 +242,6 @@ public:
// //
SourceDB &get_source() { return source_db_; } SourceDB &get_source() { return source_db_; }
// Rebuild the global environment from the source database.
//
// Error messages go to the specified actor, and also dprint.
//
// Returns true if the rebuild goes without errors.
//
bool rebuild_sourcedb(int64_t actor_id);
// Update the source database from disk, then rebuild the global environment.
//
// Error messages go to the specified actor, and also dprint.
//
// Returns true if the update goes without errors.
//
bool update_source(const util::LuaSourceVec &source, int64_t actor_id);
bool update_source(std::string_view sourcepk, int64_t actor_id);
// Supply an HTTP response to an outstanding HTTP request. // Supply an HTTP response to an outstanding HTTP request.
// //
void http_response(const HttpParser &response); void http_response(const HttpParser &response);

View File

@@ -4,24 +4,94 @@ makeclass('probe')
makeclass('invoke') makeclass('invoke')
makeclass('cube') makeclass('cube')
makeclass('sphere') makeclass('sphere')
makeclass('pylon')
makeclass('roster')
function pretty(t, indent, seen)
indent = indent or 0
seen = seen or {}
if type(t) ~= "table" then return tostring(t) end
if seen[t] then return "<cycle>" end
seen[t] = true
local spacing = string.rep(" ", indent)
local out = "{\n"
for k, v in pairs(t) do
out = out .. spacing .. " [" .. tostring(k) .. "] = "
out = out .. pretty(v, indent + 1, seen) .. ",\n"
end
return out .. spacing .. "}"
end
-- tangible.redirect(actor1,actor2)
-- tangible.forcedisconnect(actor)
-- tangible.keepactor(actor) -- Do not autodelete upon disconnect
-- tangible.delete(actor) -- If actor is a connected player it will disconnect
-- This gets called on every login except the admin user. -- This gets called on every login except the admin user.
function login.init() function login.init(self, config)
local player = global.get("nextplayer") local player = global.get("nextplayer")
global.set("nextplayer", player + 1) global.set("nextplayer", player + 1)
dprint("login.init initializing player ", player) printf("login.init initializing %p, player %d", self, player)
actor.player = player self.player = player
tangible.keepactor(actor) -- do not delete this login when the client disconnects self.color={0,0,0,0,0,0,0,0,0,0,0,0}
tangible.animinit{tan=actor, anim={bp="character", mesh="manny", plane="earth", xyz={player * 100, 0, 90}}} self.kills=0
self.killed=0
tangible.animinit{tan=self, anim={bp="character", mesh="manny", plane="earth", xyz={player * 100, 0, 90}}}
tangible.start(self, login.startscanning)
end
function login.startscanning()
printf("Scanning started. Actor=%p Place=%p", actor, place)
while true do
login.onescan()
wait(1)
end
end
function login.onescan()
local nearby=tangible.find{plane=tangible.animfinal(actor).plane,center=tangible.animfinal(actor).xyz,radius=1000,shape="cylinder"}
for k,v in pairs(nearby) do
if classname(v)=="pylon" then actor.color[v.color]=time()+30 end
if classname(v)=="player" then
if actor.team==v.team then
for ck,cv in v.color do actor.color[ck]=math.max(actor.color[ck],cv) end
else
local score1=0
local score2=0
for ck,cv in actor.color do if time()-cv<0 then score1=score1+1 end end
for ck,cv in v.color do if time()-cv<0 then score2=score2+1 end end
if score1-score2>0 then
-- kill player 2
end
end
end
end
end end
-- This gets called on the admin user. You can call login.init in here if you want. -- This gets called on the admin user. You can call login.init in here if you want.
function world.init() function world.init(self, config)
dprint("world.init") print("world.init")
global.set("nextplayer", 0) global.set("nextplayer", 0)
tangible.build{class=roster, anim={plane="earth", xyz={2000,0,0} } }
tangible.build{class=cube, anim={plane="earth", xyz={500,-100,0}, mat_color={1,0,0}}} tangible.build{class=cube, anim={plane="earth", xyz={500,-100,0}, mat_color={1,0,0}}}
tangible.build{class=sphere, anim={plane="earth", xyz={500,100,0}, mat_color={0,0,1}}} tangible.build{class=sphere, anim={plane="earth", xyz={500,100,0}, mat_color={0,0,1}}}
login.init() login.init(self, config)
end
function roster.init(self,config)
self.character={}
end
function world.buildpylon()
print("Building pylon")
local radius=1000
tangible.build{class=pylon,plane="earth",xyz={math.random(-radius,radius),math.random(-radius,radius),0}}.color=math.random(1,12)
end end
function invoke.move(action, xyz, facing) function invoke.move(action, xyz, facing)
@@ -38,20 +108,60 @@ function login.lookmenu(add)
add("Redirect", function() tangible.redirect(actor, place) end) add("Redirect", function() tangible.redirect(actor, place) end)
end end
function cube.lookmenu(add) -- Four cases: Unnamed to new, Unnamed to existing, Named to new, Named to Existing
add("Cube A", function () dprint("Doing Cube A") end) function playas(who)
add("Cube B", function () dprint("Doing Cube B") end) printf("Playas %lP",place)
add("Cube C", function () dprint("Doing Cube C") end) if not actor.name and not place.character[who] then -- Unnamed to new
add("Cube Hi", function () dprint("Doing Cube Hi") end) printf("Case 1: Naming this character "..who)
add("Cube Bye", function () dprint("Doing Cube Bye") end) actor.name=who
add("Cube Yo", function () dprint("Doing Cube Yo") end) tangible.keepactor(actor)
add("Cube Z", function () dprint("Doing Cube Z") end) place.character[who]=actor
elseif actor.name and not place.character[who] then -- Named to new (Hard one)
printf("Case 2: Creating new character "..who)
printf("Case 2.0")
local nc=tangible.build{class=login, anim={plane="earth", xyz={0,0,0} } }
printf("Case 2.1")
nc.name=who
printf("Case 2.2")
tangible.keepactor(nc)
printf("Case 2.3")
place.character[who]=nc
printf("Case 2.4: ...Place is now "..place)
tangible.redirect(actor,nc)
elseif not actor.name and place.character[who] then
printf("Case 3: Logging in from unnamed to existing character "..who)
tangible.redirect(actor,place.character[who])
elseif actor.name and place.character[who] then
printf("Case 4: Logging in from "..actor.name.." to existing character "..who)
tangible.redirect(actor,place.character[who])
end
end end
function roster.lookmenu(add)
for _,name in ipairs { "Albert", "Betty", "Cornelius" } do
add("Play as "..name..(place.character[name] and "" or "*"), function() playas(name) end)
end
end
function cube.lookmenu(add)
add("Cube A", function () print("Doing Cube A") end)
add("Cube B", function () print("Doing Cube B") end)
add("Cube C", function () print("Doing Cube C") end)
add("Cube Hi", function () print("Doing Cube Hi") end)
add("Cube Bye", function () print("Doing Cube Bye") end)
add("Cube Yo", function () print("Doing Cube Yo") end)
add("Cube Z", function () print("Doing Cube Z") end)
end
function sphere.lookhotkeys(add) function sphere.lookhotkeys(add)
add("FaceL", "Sphere Hi", function () dprint("Doing Sphere Hi") end) add("FaceL", "Sphere Hi", function () print("Doing Sphere Hi") end)
add("FaceM", "Sphere Bye", function () dprint("Doing Sphere Bye") end) add("FaceM", "Sphere Bye", function () print("Doing Sphere Bye") end)
add("FaceR", "Sphere Yo", function () dprint("Doing Sphere Yo") end) add("FaceR", "Sphere Yo", function () print("Doing Sphere Yo") end)
end
function sphere.tick(foo)
print("Tick")
end end

View File