add new zone box visuals,

This commit is contained in:
Redo
2025-07-19 21:36:50 -07:00
parent a5ce98b31d
commit 6446a621c6
13 changed files with 165 additions and 41 deletions

View File

@ -1,4 +1,3 @@
//bls 4
// This file should not exist. Fix later... // This file should not exist. Fix later...
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -61,6 +60,8 @@ function NDM_BoxSelect::onStartMode(%this, %client, %lastMode)
//Switch away from this mode //Switch away from this mode
function NDM_BoxSelect::onChangeMode(%this, %client, %nextMode) function NDM_BoxSelect::onChangeMode(%this, %client, %nextMode)
{ {
%client.ndBoxCleared();
if(%nextMode == $NDM::StackSelect) if(%nextMode == $NDM::StackSelect)
{ {
//Clear selection //Clear selection
@ -107,6 +108,8 @@ function NDM_BoxSelect::onChangeMode(%this, %client, %nextMode)
//Kill this mode //Kill this mode
function NDM_BoxSelect::onKillMode(%this, %client) function NDM_BoxSelect::onKillMode(%this, %client)
{ {
%client.ndBoxCleared();
//Destroy selection //Destroy selection
if(isObject(%client.ndSelection)) if(isObject(%client.ndSelection))
%client.ndSelection.delete(); %client.ndSelection.delete();
@ -164,6 +167,7 @@ function NDM_BoxSelect::onSelectObject(%this, %client, %obj, %pos, %normal)
else else
{ {
%client.ndSelectionBox.zoneBrick = ""; %client.ndSelectionBox.zoneBrick = "";
%client.ndSelectionBox.setNormalColors();
%box = %obj.getWorldBox(); %box = %obj.getWorldBox();
%p1 = getWords(%box, 0, 2); %p1 = getWords(%box, 0, 2);
%p2 = getWords(%box, 3, 5); %p2 = getWords(%box, 3, 5);
@ -186,6 +190,7 @@ function NDM_BoxSelect::onSelectObject(%this, %client, %obj, %pos, %normal)
} else if (isObject(%obj.physicalZone)) { } else if (isObject(%obj.physicalZone)) {
%box = ndGetBoxFromZone(%obj); %box = ndGetBoxFromZone(%obj);
%client.ndSelectionBox.zoneBrick = %obj; %client.ndSelectionBox.zoneBrick = %obj;
%client.ndSelectionBox.setZoneColors();
} else { } else {
%box = ndGetPlateBoxFromRayCast(%pos, %normal); %box = ndGetPlateBoxFromRayCast(%pos, %normal);
} }
@ -208,7 +213,7 @@ function ndRound(%v, %step) {
return mFloor(%v/%step + 0.5)*%step; return mFloor(%v/%step + 0.5)*%step;
} }
function ndCorrectBox(%box) { function ndSnapBoxToGrid(%box) {
%xl = ndRound(getWord(%box, 0), 0.5); %xl = ndRound(getWord(%box, 0), 0.5);
%yl = ndRound(getWord(%box, 1), 0.5); %yl = ndRound(getWord(%box, 1), 0.5);
%zl = ndRound(getWord(%box, 2), 0.2); %zl = ndRound(getWord(%box, 2), 0.2);
@ -226,19 +231,16 @@ function ndGetBoxFromRom(%b) {
%bh = getWords(%box, 3, 5); %bh = getWords(%box, 3, 5);
%bl = vectorAdd(%bl, "0 0 " SPC (%db.brickSizeZ * 0.2)); %bl = vectorAdd(%bl, "0 0 " SPC (%db.brickSizeZ * 0.2));
%bh = vectorAdd(%bh, "0 0 " SPC (%db.logicRomZ * 0.2)); %bh = vectorAdd(%bh, "0 0 " SPC (%db.logicRomZ * 0.2));
return ndCorrectBox(%bl SPC %bh); return ndSnapBoxToGrid(%bl SPC %bh);
} }
function ndGetBoxFromZone(%b) { function ndGetBoxFromZone(%b) {
%z = %b.physicalZone; %z = %b.physicalZone;
%pos = %z.position; %pos = %z.position;
%scale = %z.getScale(); %scale = %z.getScale();
%sx = getWord(%scale, 0); %bl = vectorAdd(%pos, 0 SPC -%getWord(%scale,1) SPC 0);
%sy = getWord(%scale, 1); %bh = vectorAdd(%pos, getWord(%scale,0) SPC 0 SPC getWord(%scale,2));
%sz = getWord(%scale, 2); return ndSnapBoxToGrid(%bl SPC %bh);
%bl = vectorAdd(%pos, 0 SPC -%sy SPC 0);
%bh = vectorAdd(%pos, %sx SPC 0 SPC %sz);
return ndCorrectBox(%bl SPC %bh);
} }
@ -366,12 +368,18 @@ function ndApplyZoneEvent(%brick, %p1a, %p2a, %client) {
if(%brick.eventOutput[%i] $= "setZoneBox") { if(%brick.eventOutput[%i] $= "setZoneBox") {
%brick.eventOutputParameter[%i, 1] = %p1; %brick.eventOutputParameter[%i, 1] = %p1;
%brick.eventOutputParameter[%i, 2] = %p2; %brick.eventOutputParameter[%i, 2] = %p2;
// update event
%brick.setZoneBox(%p1,%p2, %brick.eventOutputParameter[%i,3], 0);
if(isFunction("fxDtsBrick", "triggerPropertyOnLoad"))
%brick.triggerPropertyOnLoad();
// inform client
messageClient(%client, 'MsgError', ""); messageClient(%client, 'MsgError', "");
commandToClient(%client, 'centerPrint', commandToClient(%client, 'centerPrint',
"<font:Verdana:20>\c6Applied selection box to zone event \c3" @ %i "<font:Verdana:20>\c6Applied selection box to zone event \c3" @ %i
@ "\c6 on brick, and re-created zone.", 5); @ "\c6 on brick,<br>\c6re-created zone, and triggered propertyOnLoad events.", 5);
%brick.setZoneBox(%p1, %p2,
%brick.eventOutputParameter[%i,3], 0);
return; return;
} }
} }

View File

@ -16,24 +16,29 @@ function gameConnection::ndCreateSelectionBox(%client) {
%client.ndSelectionBox = ND_SelectionBox(%shapeName); %client.ndSelectionBox = ND_SelectionBox(%shapeName);
} }
function gameConnection::ndPushBoxHistory(%client) { function gameConnection::ndPushBoxHistory(%client, %optBox) {
//talk("push"); if(%optBox $= "") {
if(!isObject(%client.ndSelectionBox)) return; if(!isObject(%client.ndSelectionBox)) return;
%box = %client.ndSelectionBox.point1 SPC %client.ndSelectionBox.point2; %box = %client.ndSelectionBox.point1 SPC %client.ndSelectionBox.point2;
%zone = %client.ndSelectionBox.zoneBrick;
} else {
%box = %optBox;
%zone = "";
}
%entry = %box TAB %zone;
if(%client.ndBoxHistoryCount>0 && if(%client.ndBoxHistoryCount>0 &&
%box $= %client.ndBoxHistory[%client.ndBoxHistoryCount-1]) %entry $= %client.ndBoxHistory[%client.ndBoxHistoryCount-1])
return; return;
%client.ndBoxHistory[%client.ndBoxHistoryCount+0] = %box; %client.ndBoxHistory[%client.ndBoxHistoryCount+0] = %entry;
%client.ndBoxHistoryCount++; %client.ndBoxHistoryCount++;
%client.ndBoxHistoryPos = 1; // recall prior to this one %client.ndBoxHistoryPos = 1; // recall prior to this one
} }
function gameConnection::ndBoxChanged(%client) { function gameConnection::ndBoxChanged(%client) {
//talk("changed");
%client.ndBoxHistoryPos = 0; // recall starting with this initial selection %client.ndBoxHistoryPos = 0; // recall starting with this initial selection
%client.ndSaveBoxOnRecall = true; %client.ndSaveBoxOnRecall = true;
} }
function gameConnection::ndBoxCleared(%client) { function gameConnection::ndBoxCleared(%client) {
//talk("cleared");
if(%client.ndSaveBoxOnRecall) { if(%client.ndSaveBoxOnRecall) {
%client.ndPushBoxHistory(); %client.ndPushBoxHistory();
%client.ndSaveBoxOnRecall = false; %client.ndSaveBoxOnRecall = false;
@ -41,18 +46,26 @@ function gameConnection::ndBoxCleared(%client) {
%client.ndBoxHistoryPos = 0; %client.ndBoxHistoryPos = 0;
} }
function gameConnection::ndRecallBoxHistory(%client, %offset) { function gameConnection::ndRecallBoxHistory(%client, %offset) {
//talk("recall " @ %offset);
if(%offset<1 || %offset>%client.ndBoxHistoryCount) return; if(%offset<1 || %offset>%client.ndBoxHistoryCount) return;
%idx = %client.ndBoxHistoryCount - %offset; %idx = %client.ndBoxHistoryCount - %offset;
%box = %client.ndBoxHistory[%idx]; %entry = %client.ndBoxHistory[%idx];
%box = getField(%entry, 0);
%zone = getField(%entry, 1);
%p1 = getWords(%box,0,2); %p1 = getWords(%box,0,2);
%p2 = getWords(%box,3,5); %p2 = getWords(%box,3,5);
if(!isObject(%client.ndSelectionBox)) if(!isObject(%client.ndSelectionBox))
%client.ndCreateSelectionBox(); %client.ndCreateSelectionBox();
%client.ndSelectionBox.zoneBrick = %zone;
if(isObject(%zone))
%client.ndSelectionBox.setZoneColors();
else
%client.ndSelectionBox.setNormalColors();
%client.ndSelectionBox.setSize(%p1,%p2); %client.ndSelectionBox.setSize(%p1,%p2);
%client.ndUpdateBottomPrint(); %client.ndUpdateBottomPrint();
ndBoxPlaySound(%client.ndSelectionBox, BrickMoveSound); ndBoxPlaySound(%client.ndSelectionBox, BrickMoveSound);
} }

View File

@ -1,4 +1,3 @@
//bls 4
// This file should not exist. Fix later... // This file should not exist. Fix later...
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -273,6 +272,8 @@ function NDM_PlantCopy::conditionalPlant(%this, %client, %force, %ownership)
return; return;
} }
%client.ndPushBoxHistory(%client.ndSelection.getGhostWorldBox());
%client.ndLastPlantTime = $Sim::Time; %client.ndLastPlantTime = $Sim::Time;
%pos = %client.ndSelection.ghostPosition; %pos = %client.ndSelection.ghostPosition;

View File

@ -1,4 +1,3 @@
//bls 4
// This file should not exist. Fix later... // This file should not exist. Fix later...
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -1,4 +1,3 @@
//bls 3
// This file is way too big. Fix later... // This file is way too big. Fix later...
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -55,11 +55,27 @@ function ND_SelectionBox::onRemove(%this)
%this.border_y[%i].delete(); %this.border_y[%i].delete();
%this.border_z[%i].delete(); %this.border_z[%i].delete();
} }
ndShapelineConnectBoxes_delete(%this);
} }
//Set normal color values and borders function ND_SelectionBox::setZoneColors(%this) {
function ND_SelectionBox::setNormalMode(%this) %this.innerColor = "0 0 0 0.60";
{ %this.outerColor = "0 0 0 0.35";
%this.borderColor = "1 0.7 0.5 0.99";
%this.borderColorSelected = "0.7 0 1 0.99";
%this.cornerColor1 = "0.8 0.74 0.5 0.99";
%this.cornerColor2 = "1 0.94 0.5 0.99";
%this.cornerColorSelected1 = "0.7 0.2 1 0.99";
%this.cornerColorSelected2 = "0.7 0.1 1 0.99";
%this.applyColors();
}
function ND_SelectionBox::setNormalColors(%this) {
%this.innerColor = "0 0 0 0.60"; %this.innerColor = "0 0 0 0.60";
%this.outerColor = "0 0 0 0.35"; %this.outerColor = "0 0 0 0.35";
@ -71,7 +87,13 @@ function ND_SelectionBox::setNormalMode(%this)
%this.cornerColorSelected1 = "0 0.2 1 0.99"; %this.cornerColorSelected1 = "0 0.2 1 0.99";
%this.cornerColorSelected2 = "0 0.1 0.9 0.99"; %this.cornerColorSelected2 = "0 0.1 0.9 0.99";
%this.applyColors();
}
//Set normal color values and borders
function ND_SelectionBox::setNormalMode(%this)
{
%this.isNormalMode = true; %this.isNormalMode = true;
//Unhide the corners and inner/outer box (hidden in disabled mode) //Unhide the corners and inner/outer box (hidden in disabled mode)
@ -83,7 +105,7 @@ function ND_SelectionBox::setNormalMode(%this)
%this.outerBox.unHideNode("ALL"); %this.outerBox.unHideNode("ALL");
//Apply changes //Apply changes
%this.applyColors(); %this.setNormalColors();
%this.setSize(%this.point1, %this.point2); %this.setSize(%this.point1, %this.point2);
%this.shapeName.setShapeName(%this.boxName); %this.shapeName.setShapeName(%this.boxName);
} }
@ -307,6 +329,16 @@ function ND_SelectionBox::setSize(%this, %point1, %point2)
%this.border_z[%i].setScale(%width SPC %width SPC %len_z + %width * 0.05); %this.border_z[%i].setScale(%width SPC %width SPC %len_z + %width * 0.05);
} }
} }
// Draw lines from zone brick to zone if present
if(isObject(%this.zoneBrick)) {
ndShapelineConnectBoxes_create(%this, "1 0.7 0.5 0.3");
ndShapelineConnectBoxes_move(%this,
%this.zoneBrick.getWorldBox(),
%this.point1 SPC %this.point2);
} else {
ndShapelineConnectBoxes_delete(%this);
}
} }
//Resize the selection box and align it to a player //Resize the selection box and align it to a player

View File

@ -5,18 +5,18 @@ A fork of the original New Duplicator by Zeblote, presently maintained and with
- Added the `/ownership` or `/o` command, which plants each brick in the current selection with its original ownership. Use in plant mode after copying/cutting. - Added the `/ownership` or `/o` command, which plants each brick in the current selection with its original ownership. Use in plant mode after copying/cutting.
`/savedup` now saves ownership, and `/ownership` can be used while holding a loaded duplication to load it. `/savedup` now saves ownership, and `/ownership` can be used while holding a loaded duplication to load it.
(Loading ownership only works if the duplication was saved with this version, as the original newdup does not save ownership) (Loading ownership only works if the duplication was saved with this version, as the original newdup does not save ownership)
Use `/toggleownership` or `/to` to always plant with original ownership, similar to `/toggleforceplant` or `/tfp` Use `/toggleOwnership` or `/to` to always plant with original ownership.
- Added `/prevBox` and `/nextBox` (aliases `/pb` and `/nb`) to recall selection-box history when in box-select mode, similar to pressing up in a chat client or terminal. - Added `/prevBox` and `/nextBox` (aliases `/pb` and `/nb`) to recall selection box history when in box-select mode, similar to pressing up in a chat client or terminal.
- Holding ctrl while making an initial stack selection selects all adjacent bricks of the same color, ignoring diagonals, similar to the fill-can. - Holding ctrl while making an initial stack selection selects all adjacent bricks of the same color, similar to the fill-can, ignoring diagonals.
With limited mode off, all adjacent bricks will be selected regardless of color. With limited mode off, all adjacent bricks will be selected regardless of color.
- The `/alldups` list is nicely formatted, sorted by date, and shows who saved each item. - The `/alldups` list is nicely formatted, sorted by date, and shows who saved each item.
- Support for Brick_LuaLogic - Support for Brick_LuaLogic:
Supercut can be used on wire bricks. Supercut can be used on wire bricks.
`/FillBrick LogicWire` or `/fbw` can be used to fill with wire bricks. `/fillBrick LogicWire` or `/fbw` can be used to fill with wire bricks.
Initial-multi-box-selecting a ROM sets the box to its data volume. Initial-multi-box-selecting a ROM sets the box to its data volume.
- Support for Event_setZoneBox - Support for Event_setZoneBox:
Initial multi-box-selecting a brick with a zone sets the box to its zone. Initial multi-box-selecting a brick with a zone sets the box to its zone.
Hitting plant with this box updates the first Event_SetZoneBox event if present. Hitting plant with this box updates the first setZoneBox output event if present.
## Tweaks ## Tweaks
- Made the "Create Sym Table on Start" pref default to true. - Made the "Create Sym Table on Start" pref default to true.
@ -26,6 +26,6 @@ Hitting plant with this box updates the first Event_SetZoneBox event if present.
## Fixes ## Fixes
- Fixed "Nonexistent undo state" message when undoing a plant that has been supercut. - Fixed "Nonexistent undo state" message when undoing a plant that has been supercut.
- Fixed preventing unequipping any tool for 1.5s after using `/duplicator` - Fixed preventing unequipping any tool for 1.5s after using `/duplicator`
- Removed useless files from the add-on root directory - Removed useless files from the add-on root directory.
- Removed the annoying messages about mismatched newdup versions when joining a server. - Removed the annoying messages about mismatched newdup versions when joining a server.
- Removed the worm known as Support_Updater. - Removed the worm known as Support_Updater.

View File

@ -46,6 +46,7 @@ function serverCmdDupHelp(%client)
messageClient(%client, '', "<tab:220>\c3/ToggleForcePlant\t\c6 Enable force plant for normal planting, so you dont have to type it all the time."); messageClient(%client, '', "<tab:220>\c3/ToggleForcePlant\t\c6 Enable force plant for normal planting, so you dont have to type it all the time.");
messageClient(%client, '', "<tab:220>\c3/PlantAs\c6 [\c3target\c6]\t\c6 Plant bricks in a different brick group. Target can be a name or blid."); messageClient(%client, '', "<tab:220>\c3/PlantAs\c6 [\c3target\c6]\t\c6 Plant bricks in a different brick group. Target can be a name or blid.");
messageClient(%client, '', "<tab:220>\c3/Ownership\t\c6 Plant bricks with the original ownership from when they were copied or saved."); messageClient(%client, '', "<tab:220>\c3/Ownership\t\c6 Plant bricks with the original ownership from when they were copied or saved.");
messageClient(%client, '', "<tab:220>\c3/ToggleOwnership\t\c6 Always plant bricks with original ownership.");
messageClient(%client, '', "<font:Arial:8> "); messageClient(%client, '', "<font:Arial:8> ");
messageClient(%client, '', "<tab:220>\c3/FillWrench\t\c6 Open the fill wrench gui to change settings on all selected bricks."); messageClient(%client, '', "<tab:220>\c3/FillWrench\t\c6 Open the fill wrench gui to change settings on all selected bricks.");

View File

@ -1,4 +1,3 @@
//bls 3
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
//bls 3
// Handles interactions with the handheld duplicator item. // Handles interactions with the handheld duplicator item.
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -1,6 +1,4 @@
//bls 3
//function ndRotateVector(%vector, %steps)
function ndFillBus(%pos1, %pos2, %angleId, %color) { function ndFillBus(%pos1, %pos2, %angleId, %color) {
talk(%pos1 @ ", " @ %pos2 @ ", " @ %angleId); talk(%pos1 @ ", " @ %pos2 @ ", " @ %angleId);
$ND::FillBrickColorID = %color + 1; $ND::FillBrickColorID = %color + 1;

View File

@ -0,0 +1,73 @@
function ndShapelineCreate(%color) {
if(%color$="") %color="1 1 1 1";
%line = new StaticShape() {
datablock = ND_SelectionBoxBorder;
};
//%line.setScopeAlways(); // newdup does this, idk why yet
%line.setNodeColor("ALL", %color);
return %line;
}
function ndShapelineMove(%line, %p1,%p2) {
// calculate shape position
%center = vectorScale(vectorAdd(%p1,%p2), 0.5);
// calculate shape rotation
%lineDir = "0 0 1"; // direction of static shape
%dir = vectorSub(%p2, %p1);
%axis = vectorNormalize(vectorCross(%dir, %lineDir));
%angle = mAcos(vectorDot(%dir, %lineDir) / vectorLen(%dir) / vectorLen(%lineDir));
%rot = %axis SPC %angle;
// calculate shape scale
%scale = "1 1" SPC %vectorLen(%dir);
%line.setTransform(%center SPC %rot);
%line.setScale(%scale);
}
function ndShapelineDelete(%line) {
%line.delete();
}
////////////////////////////////////////////////////////////////////////////////
// Box drawing
function ndShapelineConnectBoxes_delete(%container) { // public
for(%i=0; %i<8; %i++) {
if(isObject(%line = %container.ndShapeline[%i]))
%line.delete();
}
}
function ndShapelineConnectBoxes_create(%container, %color) { // public
for(%i=0; %i<8; %i++) {
if(!isObject(%container.ndShapeline[%i]))
%container.ndShapeline[%i] = ndShapelineCreate(%color);
}
}
function ndMin(%a,%b) { return %a<%b ? %a : %b; }
function ndMax(%a,%b) { return %a>%b ? %a : %b; }
// draw 8 lines between the boxes' 8 corners
function ndNormalizeBox(%box) {
%pLx = ndMin(getWord(%box,0),getWord(%box,3));
%pLy = ndMin(getWord(%box,1),getWord(%box,4));
%pLz = ndMin(getWord(%box,2),getWord(%box,5));
%pHx = ndMax(getWord(%box,0),getWord(%box,3));
%pHy = ndMax(getWord(%box,1),getWord(%box,4));
%pHz = ndMax(getWord(%box,2),getWord(%box,5));
return %pLx SPC %pLy SPC %pLz SPC %pHx SPC %pHy SPC %pHz;
}
function ndShapelineConnectBoxes_move(%container, %box1, %box2) { // public
%box1 = ndNormalizeBox(%box1);
%box2 = ndNormalizeBox(%box2);
ndShapelineMove(%container.ndShapeline[0], getWord(%box1,0) SPC getWord(%box1,1) SPC getWord(%box1,2), getWord(%box2,0) SPC getWord(%box2,1) SPC getWord(%box2,2));
ndShapelineMove(%container.ndShapeline[1], getWord(%box1,0) SPC getWord(%box1,1) SPC getWord(%box1,5), getWord(%box2,0) SPC getWord(%box2,1) SPC getWord(%box2,5));
ndShapelineMove(%container.ndShapeline[2], getWord(%box1,0) SPC getWord(%box1,4) SPC getWord(%box1,2), getWord(%box2,0) SPC getWord(%box2,4) SPC getWord(%box2,2));
ndShapelineMove(%container.ndShapeline[3], getWord(%box1,0) SPC getWord(%box1,4) SPC getWord(%box1,5), getWord(%box2,0) SPC getWord(%box2,4) SPC getWord(%box2,5));
ndShapelineMove(%container.ndShapeline[4], getWord(%box1,3) SPC getWord(%box1,1) SPC getWord(%box1,2), getWord(%box2,3) SPC getWord(%box2,1) SPC getWord(%box2,2));
ndShapelineMove(%container.ndShapeline[5], getWord(%box1,3) SPC getWord(%box1,1) SPC getWord(%box1,5), getWord(%box2,3) SPC getWord(%box2,1) SPC getWord(%box2,5));
ndShapelineMove(%container.ndShapeline[6], getWord(%box1,3) SPC getWord(%box1,4) SPC getWord(%box1,2), getWord(%box2,3) SPC getWord(%box2,4) SPC getWord(%box2,2));
ndShapelineMove(%container.ndShapeline[7], getWord(%box1,3) SPC getWord(%box1,4) SPC getWord(%box1,5), getWord(%box2,3) SPC getWord(%box2,4) SPC getWord(%box2,5));
}

View File

@ -49,6 +49,7 @@ exec($ND::ScriptPath @ "server/images.cs");
exec($ND::ScriptPath @ "server/modes.cs"); exec($ND::ScriptPath @ "server/modes.cs");
exec($ND::ScriptPath @ "server/namedtargets.cs"); exec($ND::ScriptPath @ "server/namedtargets.cs");
exec($ND::ScriptPath @ "server/prefs.cs"); exec($ND::ScriptPath @ "server/prefs.cs");
exec($ND::ScriptPath @ "server/shapeline.cs");
exec($ND::ScriptPath @ "server/symmetrydefinitions.cs"); exec($ND::ScriptPath @ "server/symmetrydefinitions.cs");
exec($ND::ScriptPath @ "server/symmetrytable.cs"); exec($ND::ScriptPath @ "server/symmetrytable.cs");
exec($ND::ScriptPath @ "server/undo.cs"); exec($ND::ScriptPath @ "server/undo.cs");