From 93d385e859beec02bd9ba53d38dbb605ecc998eb Mon Sep 17 00:00:00 2001 From: Redo Date: Wed, 5 Oct 2022 16:02:11 -0600 Subject: [PATCH] initial commit --- classes/server/duplimode/boxselect.cs | 462 ++ classes/server/duplimode/boxselectprogress.cs | 59 + classes/server/duplimode/cutprogress.cs | 48 + classes/server/duplimode/fillcolor.cs | 128 + classes/server/duplimode/fillcolorprogress.cs | 41 + classes/server/duplimode/loadprogress.cs | 76 + classes/server/duplimode/plantcopy.cs | 268 + classes/server/duplimode/plantcopyprogress.cs | 62 + classes/server/duplimode/saveprogress.cs | 43 + classes/server/duplimode/stackselect.cs | 210 + .../server/duplimode/stackselectprogress.cs | 43 + classes/server/duplimode/supercutprogress.cs | 50 + classes/server/duplimode/wrenchprogress.cs | 45 + classes/server/ghostgroup.cs | 38 + classes/server/highlightbox.cs | 112 + classes/server/selection.cs | 4494 +++++++++++++++++ classes/server/selectionbox.cs | 496 ++ classes/server/undogrouppaint.cs | 75 + classes/server/undogroupplant.cs | 61 + classes/server/undogroupwrench.cs | 197 + description.txt | 4 + resources/server/black.png | Bin 0 -> 82 bytes resources/server/blank.png | Bin 0 -> 84 bytes resources/server/blue.png | Bin 0 -> 81 bytes resources/server/brickBOTTOMEDGE.png | Bin 0 -> 76266 bytes resources/server/brickBOTTOMLOOP.png | Bin 0 -> 75221 bytes resources/server/brickSIDE.png | Bin 0 -> 10058 bytes resources/server/brickTOP.png | Bin 0 -> 44652 bytes resources/server/duplicator_brick.dts | Bin 0 -> 14772 bytes resources/server/duplicator_selection.dts | Bin 0 -> 19684 bytes resources/server/icon.png | Bin 0 -> 3019 bytes resources/server/selectionbox_border.dts | Bin 0 -> 3785 bytes resources/server/selectionbox_inner.dts | Bin 0 -> 2405 bytes resources/server/selectionbox_outer.dts | Bin 0 -> 2409 bytes resources/server/transparent.png | Bin 0 -> 84 bytes resources/server/white.png | Bin 0 -> 133 bytes scripts/common/bytetable.cs | 225 + scripts/server/commands.cs | 1151 +++++ scripts/server/datablocks.cs | 344 ++ scripts/server/functions.cs | 734 +++ scripts/server/handshake.cs | 71 + scripts/server/highlight.cs | 106 + scripts/server/images.cs | 297 ++ scripts/server/modes.cs | 497 ++ scripts/server/namedtargets.cs | 90 + scripts/server/prefs.cs | 212 + scripts/server/symmetrydefinitions.cs | 180 + scripts/server/symmetrytable.cs | 692 +++ scripts/server/undo.cs | 31 + server.cs | 65 + v20fix.cs | 15 + 51 files changed, 11722 insertions(+) create mode 100644 classes/server/duplimode/boxselect.cs create mode 100644 classes/server/duplimode/boxselectprogress.cs create mode 100644 classes/server/duplimode/cutprogress.cs create mode 100644 classes/server/duplimode/fillcolor.cs create mode 100644 classes/server/duplimode/fillcolorprogress.cs create mode 100644 classes/server/duplimode/loadprogress.cs create mode 100644 classes/server/duplimode/plantcopy.cs create mode 100644 classes/server/duplimode/plantcopyprogress.cs create mode 100644 classes/server/duplimode/saveprogress.cs create mode 100644 classes/server/duplimode/stackselect.cs create mode 100644 classes/server/duplimode/stackselectprogress.cs create mode 100644 classes/server/duplimode/supercutprogress.cs create mode 100644 classes/server/duplimode/wrenchprogress.cs create mode 100644 classes/server/ghostgroup.cs create mode 100644 classes/server/highlightbox.cs create mode 100644 classes/server/selection.cs create mode 100644 classes/server/selectionbox.cs create mode 100644 classes/server/undogrouppaint.cs create mode 100644 classes/server/undogroupplant.cs create mode 100644 classes/server/undogroupwrench.cs create mode 100644 description.txt create mode 100644 resources/server/black.png create mode 100644 resources/server/blank.png create mode 100644 resources/server/blue.png create mode 100644 resources/server/brickBOTTOMEDGE.png create mode 100644 resources/server/brickBOTTOMLOOP.png create mode 100644 resources/server/brickSIDE.png create mode 100644 resources/server/brickTOP.png create mode 100644 resources/server/duplicator_brick.dts create mode 100644 resources/server/duplicator_selection.dts create mode 100644 resources/server/icon.png create mode 100644 resources/server/selectionbox_border.dts create mode 100644 resources/server/selectionbox_inner.dts create mode 100644 resources/server/selectionbox_outer.dts create mode 100644 resources/server/transparent.png create mode 100644 resources/server/white.png create mode 100644 scripts/common/bytetable.cs create mode 100644 scripts/server/commands.cs create mode 100644 scripts/server/datablocks.cs create mode 100644 scripts/server/functions.cs create mode 100644 scripts/server/handshake.cs create mode 100644 scripts/server/highlight.cs create mode 100644 scripts/server/images.cs create mode 100644 scripts/server/modes.cs create mode 100644 scripts/server/namedtargets.cs create mode 100644 scripts/server/prefs.cs create mode 100644 scripts/server/symmetrydefinitions.cs create mode 100644 scripts/server/symmetrytable.cs create mode 100644 scripts/server/undo.cs create mode 100644 server.cs create mode 100644 v20fix.cs diff --git a/classes/server/duplimode/boxselect.cs b/classes/server/duplimode/boxselect.cs new file mode 100644 index 0000000..a6f8e50 --- /dev/null +++ b/classes/server/duplimode/boxselect.cs @@ -0,0 +1,462 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Switch to this mode +function NDM_BoxSelect::onStartMode(%this, %client, %lastMode) +{ + if(%lastMode == $NDM::StackSelect) + { + if(isObject(%client.ndSelection) && %client.ndSelection.brickCount) + { + //Create selection box from the size of the previous selection + %root = %client.ndSelection.rootPosition; + %min = vectorAdd(%root, %client.ndSelection.minSize); + %max = vectorAdd(%root, %client.ndSelection.maxSize); + + if(%client.isAdmin) + %limit = $Pref::Server::ND::MaxBoxSizeAdmin; + else + %limit = $Pref::Server::ND::MaxBoxSizePlayer; + + if((getWord(%max, 0) - getWord(%min, 0) <= %limit) + && (getWord(%max, 1) - getWord(%min, 1) <= %limit) + && (getWord(%max, 2) - getWord(%min, 2) <= %limit)) + { + %name = %client.name; + + if(getSubStr(%name, strLen(%name - 1), 1) $= "s") + %shapeName = %name @ "' Selection Box"; + else + %shapeName = %name @ "'s Selection Box"; + + %client.ndSelectionBox = ND_SelectionBox(%shapeName); + %client.ndSelectionBox.setSizeAligned(%min, %max, %client.getControlObject()); + } + else + commandToClient(%client, 'centerPrint', "\c6Oops!\n" @ + "\c6Your selection box is limited to \c3" @ mFloor(%limit * 2) @ " \c6studs.", 5); + + %client.ndSelection.deleteData(); + } + + %client.ndSelectionAvailable = false; + } + else if(%lastMode == $NDM::BoxSelectProgress && %client.ndSelection.brickCount > 0) + { + %client.ndSelectionBox.setDisabledMode(); + %client.ndSelectionAvailable = true; + } + else if(%lastMode != $NDM::FillColor && %lastMode != $NDM::WrenchProgress) + %client.ndSelectionAvailable = false; + + %client.ndLastSelectMode = %this; + %client.ndUpdateBottomPrint(); +} + +//Switch away from this mode +function NDM_BoxSelect::onChangeMode(%this, %client, %nextMode) +{ + if(%nextMode == $NDM::StackSelect) + { + //Clear selection + if(isObject(%client.ndSelection)) + %client.ndSelection.deleteData(); + + //Remove the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); + } + else if(%nextMode == $NDM::PlantCopy) + { + //Start de-highlighting the bricks + %client.ndSelection.deHighlight(); + + //Remove the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); + } + else if(%nextMode == $NDM::CutProgress) + { + //Remove the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); + } + else if(%nextMode == $NDM::FillColor) + { + //Start de-highlighting the bricks + %client.ndSelection.deHighlight(); + } + else if(%nextMode == $NDM::WrenchProgress) + { + //Start de-highlighting the bricks + %client.ndSelection.deHighlight(); + } + else if(%nextMode == $NDM::LoadProgress) + { + //Remove the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); + } +} + +//Kill this mode +function NDM_BoxSelect::onKillMode(%this, %client) +{ + //Destroy selection + if(isObject(%client.ndSelection)) + %client.ndSelection.delete(); + + //Delete the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); +} + + + +//Duplicator image callbacks +/////////////////////////////////////////////////////////////////////////// + +//Selecting an object with the duplicator +function NDM_BoxSelect::onSelectObject(%this, %client, %obj, %pos, %normal) +{ + if((%obj.getType() & $TypeMasks::FxBrickAlwaysObjectType) == 0) + return; + + if(!ndTrustCheckMessage(%obj, %client)) + return; + + if(%client.ndSelectionAvailable) + { + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6Selection canceled! " @ + "You can now edit the box again.", 5); + + %client.ndSelectionAvailable = false; + %client.ndSelection.deleteData(); + %client.ndSelectionBox.setNormalMode(); + %client.ndUpdateBottomPrint(); + } + + if(isObject(%client.ndSelectionBox)) + { + if(%client.ndMultiSelect) + { + %box1 = %client.ndSelectionBox.getWorldBox(); + //%box2 = ndGetPlateBoxFromRayCast(%pos, %normal); + %box2 = %obj.getWorldBox(); + + %p1 = getMin(getWord(%box1, 0), getWord(%box2, 0)) + SPC getMin(getWord(%box1, 1), getWord(%box2, 1)) + SPC getMin(getWord(%box1, 2), getWord(%box2, 2)); + + %p2 = getMax(getWord(%box1, 3), getWord(%box2, 3)) + SPC getMax(getWord(%box1, 4), getWord(%box2, 4)) + SPC getMax(getWord(%box1, 5), getWord(%box2, 5)); + } + else + { + %box = %obj.getWorldBox(); + %p1 = getWords(%box, 0, 2); + %p2 = getWords(%box, 3, 5); + } + } + else + { + %name = %client.name; + + if(getSubStr(%name, strLen(%name - 1), 1) $= "s") + %shapeName = %name @ "' Selection Box"; + else + %shapeName = %name @ "'s Selection Box"; + + %client.ndSelectionBox = ND_SelectionBox(%shapeName); + + // if(%client.ndMultiSelect) + // %box = ndGetPlateBoxFromRayCast(%pos, %normal); + // else + %box = %obj.getWorldBox(); + + %p1 = getWords(%box, 0, 2); + %p2 = getWords(%box, 3, 5); + } + + %client.ndSelectionBox.setSizeAligned(%p1, %p2, %client.getControlObject()); + %client.ndUpdateBottomPrint(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Light key +function NDM_BoxSelect::onLight(%this, %client) +{ + if($Pref::Server::ND::PlayMenuSounds) + %client.play2d(lightOffSound); + + %client.ndSetMode(NDM_StackSelect); +} + +//Prev Seat +function NDM_BoxSelect::onPrevSeat(%this, %client) +{ + %client.ndLimited = !%client.ndLimited; + %client.ndUpdateBottomPrint(); + + if($Pref::Server::ND::PlayMenuSounds) + %client.play2d(%client.ndLimited ? lightOnSound : lightOffSound); +} + +//Shift Brick +function NDM_BoxSelect::onShiftBrick(%this, %client, %x, %y, %z) +{ + if(!isObject(%client.ndSelectionBox)) + return; + + //If we have a selection, enter plant mode! + if(%client.ndSelectionAvailable) + { + %client.ndSetMode(NDM_PlantCopy); + NDM_PlantCopy.onShiftBrick(%client, %x, %y, %z); + + return; + } + + //Move the corner + switch(getAngleIDFromPlayer(%client.getControlObject())) + { + case 0: %newX = %x; %newY = %y; + case 1: %newX = -%y; %newY = %x; + case 2: %newX = -%x; %newY = -%y; + case 3: %newX = %y; %newY = -%x; + } + + %newX = mFloor(%newX) / 2; + %newY = mFloor(%newY) / 2; + %z = mFloor(%z ) / 5; + + if(!%client.ndMultiSelect) + { + if(%client.isAdmin) + %limit = $Pref::Server::ND::MaxBoxSizeAdmin; + else + %limit = $Pref::Server::ND::MaxBoxSizePlayer; + + if(%client.ndSelectionBox.shiftCorner(%newX SPC %newY SPC %z, %limit)) + commandToClient(%client, 'centerPrint', "\c6Oops!\n" @ + "\c6Your selection box is limited to \c3" @ mFloor(%limit * 2) @ " \c6studs.", 5); + + %client.ndUpdateBottomPrint(); + } + else + { + %client.ndSelectionBox.shift(%newX SPC %newY SPC %z); + } +} + +//Super Shift Brick +function NDM_BoxSelect::onSuperShiftBrick(%this, %client, %x, %y, %z) +{ + //If we have a selection, enter plant mode! + if(%client.ndSelectionAvailable) + { + %client.ndSetMode(NDM_PlantCopy); + NDM_PlantCopy.onSuperShiftBrick(%client, %x, %y, %z); + + return; + } + + %this.onShiftBrick(%client, %x * 8, %y * 8, %z * 20); +} + +//Rotate Brick +function NDM_BoxSelect::onRotateBrick(%this, %client, %direction) +{ + if(!isObject(%client.ndSelectionBox)) + return; + + //If we have a selection, enter plant mode! + if(%client.ndSelectionAvailable) + { + %client.ndSetMode(NDM_PlantCopy); + NDM_PlantCopy.onRotateBrick(%client, %direction); + + return; + } + + if(!%client.ndMultiSelect) + %client.ndSelectionBox.switchCorner(); + else + { + %client.ndSelectionBox.rotate(%direction); + %client.ndUpdateBottomPrint(); + } + +} + +//Plant Brick +function NDM_BoxSelect::onPlantBrick(%this, %client) +{ + if(!isObject(%client.ndSelectionBox)) + return; + + //If we have a selection, enter plant mode! + if(%client.ndSelectionAvailable) + { + %client.ndSetMode(NDM_PlantCopy); + return; + } + + //Check timeout + if(!%client.isAdmin && %client.ndLastSelectTime + ($Pref::Server::ND::SelectTimeoutMS / 1000) > $Sim::Time) + { + %remain = mCeil(%client.ndLastSelectTime + ($Pref::Server::ND::SelectTimeoutMS / 1000) - $Sim::Time); + + if(%remain != 1) + %s = "s"; + + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6You need to wait\c3 " @ + %remain @ "\c6 second" @ %s @ " before selecting again!", 5); + + return; + } + + %client.ndLastSelectTime = $Sim::Time; + + //Prepare a selection to copy the bricks + if(isObject(%client.ndSelection)) + %client.ndSelection.deleteData(); + else + %client.ndSelection = ND_Selection(%client); + + //Start selection + %box = %client.ndSelectionBox.getWorldBox(); + + %client.ndSetMode(NDM_BoxSelectProgress); + %client.ndSelection.startBoxSelection(%box, %client.ndLimited); +} + +//Cancel Brick +function NDM_BoxSelect::onCancelBrick(%this, %client) +{ + if(!isObject(%client.ndSelectionBox)) + return; + + if(%client.ndSelectionAvailable) + { + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6Selection canceled! " @ + "You can now edit the box again.", 5); + + %client.ndSelectionAvailable = false; + %client.ndSelection.deleteData(); + %client.ndSelectionBox.setNormalMode(); + %client.ndUpdateBottomPrint(); + + return; + } + + if(isObject(%client.ndSelection)) + %client.ndSelection.deleteData(); + + %client.ndSelectionBox.delete(); + %client.ndSelectionAvailable = false; + %client.ndUpdateBottomPrint(); +} + +//Copy Selection +function NDM_BoxSelect::onCopy(%this, %client) +{ + %this.onPlantBrick(%client); +} + +//Cut Selection +function NDM_BoxSelect::onCut(%this, %client) +{ + if(!isObject(%client.ndSelectionBox)) + return; + + if(!%client.ndSelectionAvailable) + { + %this.onPlantBrick(%client); + return; + } + + %client.ndSetMode(NDM_CutProgress); + %client.ndSelection.startCutting(); +} + +//Supercut selection +function NDM_BoxSelect::onSuperCut(%this, %client) +{ + if(!isObject(%client.ndSelectionBox)) + return; + + //Prepare a selection to handle the callback + if(isObject(%client.ndSelection)) + %client.ndSelection.deleteData(); + else + %client.ndSelection = ND_Selection(%client); + + if(!$ND::SimpleBrickTableCreated) + ndCreateSimpleBrickTable(); + + //Start supercut + %box = %client.ndSelectionBox.getWorldBox(); + + %client.ndSetMode(NDM_SuperCutProgress); + %client.ndSelection.startSuperCut(%box); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_BoxSelect::getBottomPrint(%this, %client) +{ + if(isObject(%client.ndSelection) && %client.ndSelection.brickCount) + { + %count = %client.ndSelection.brickCount; + %title = "Selection Mode (\c3" @ %count @ "\c6 Brick" @ (%count > 1 ? "s)" : ")"); + } + else + %title = "Selection Mode"; + + %l0 = "Type: \c3Box \c6[Light]"; + %l1 = "Limited: " @ (%client.ndLimited ? "\c3Yes" : "\c0No") @ " \c6[Prev Seat]"; + + if(isObject(%client.ndSelectionBox)) + { + %size = %client.ndSelectionBox.getSize(); + %x = mFloatLength(getWord(%size, 0) * 2, 0); + %y = mFloatLength(getWord(%size, 1) * 2, 0); + %z = mFloatLength(getWord(%size, 2) * 5, 0); + %l2 = "Size: \c3" @ %x @ "\c6 x \c3" @ %y @ "\c6 x \c3" @ %z @ "\c6 Plates"; + } + + if(!isObject(%client.ndSelectionBox)) + { + %r0 = "Click Brick: Place selection box"; + %r1 = ""; + %r2 = ""; + } + else if(!%client.ndSelectionAvailable) + { + %r0 = "[Shift Brick]: Move corner"; + %r1 = "[Rotate Brick]: Switch corner"; + } + else + { + %r0 = "[Cancel Brick]: Adjust box"; + %r1 = "[Plant Brick]: Duplicate"; + } + + return ndFormatMessage(%title, %l0, %r0, %l1, %r1, %l2, %r2); +} diff --git a/classes/server/duplimode/boxselectprogress.cs b/classes/server/duplimode/boxselectprogress.cs new file mode 100644 index 0000000..e9bf157 --- /dev/null +++ b/classes/server/duplimode/boxselectprogress.cs @@ -0,0 +1,59 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_BoxSelectProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); + + //Remove selection box + %client.ndSelectionBox.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_BoxSelectProgress::onCancelBrick(%this, %client) +{ + commandToClient(%client, 'centerPrint', "\c6Selection canceled!", 4); + + %client.ndSelection.cancelBoxSelection(); + %client.ndSetMode(NDM_BoxSelect); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_BoxSelectProgress::getBottomPrint(%this, %client) +{ + %qCount = %client.ndSelection.queueCount; + %bCount = %client.ndSelection.brickCount; + + if(%bCount <= 0) + { + %curr = %client.ndSelection.currChunk + 1; + %num = %client.ndSelection.numChunks; + + %percent = mFloor(%curr * 100 / %num); + %title = "Searching... (\c3" @ %percent @ "%\c6, \c3" @ %qCount @ "\c6 Bricks)"; + } + else + { + %percent = mFloor(%bCount * 100 / %qCount); + %title = "Processing... (\c3" @ %percent @ "%\c6)"; + } + + %l0 = "[Cancel Brick]: Cancel selection"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/cutprogress.cs b/classes/server/duplimode/cutprogress.cs new file mode 100644 index 0000000..8219996 --- /dev/null +++ b/classes/server/duplimode/cutprogress.cs @@ -0,0 +1,48 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Switch away from this mode +function NDM_CutProgress::onChangeMode(%this, %client, %nextMode) +{ + if(%nextMode != $NDM::PlantCopy) + %client.ndSelection.deleteData(); +} + +//Kill this mode +function NDM_CutProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_CutProgress::onCancelBrick(%this, %client) +{ + %client.ndSelection.cancelCutting(); + %client.ndSetMode(%client.ndLastSelectMode); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_CutProgress::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + %percent = mFloor(%client.ndSelection.cutIndex * 100 / %count); + + %title = "Cutting... (\c3" @ %percent @ "%\c6)"; + %l0 = "[Cancel Brick]: Cancel cut"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/fillcolor.cs b/classes/server/duplimode/fillcolor.cs new file mode 100644 index 0000000..3361312 --- /dev/null +++ b/classes/server/duplimode/fillcolor.cs @@ -0,0 +1,128 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Switch to this mode +function NDM_FillColor::onStartMode(%this, %client, %lastMode) +{ + %client.ndUpdateBottomPrint(); + cancel(%client.ndToolSchedule); +} + +//Switch away from this mode +function NDM_FillColor::onChangeMode(%this, %client, %nextMode) +{ + //Hide paint gui + if(%nextMode != $NDM::FillColorProgress) + { + %client.ndLastEquipTime = $Sim::Time; + + if(%client.ndEquippedFromItem) + commandToClient(%client, 'setScrollMode', 2); + else + commandToClient(%client, 'setScrollMode', 3); + } +} + +//Kill this mode +function NDM_FillColor::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); + + //Remove the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Plant Brick +function NDM_FillColor::onPlantBrick(%this, %client) +{ + //Admin limit + if($Pref::Server::ND::PaintAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Paint Mode is admin only. Ask an admin for help."); + return; + } + + //Normal colors + if(%client.currentFxColor $= "") + { + %client.ndSetMode(NDM_FillColorProgress); + %client.ndSelection.startFillColor(0, %client.currentColor); + return; + } + + //Admin limit + if($Pref::Server::ND::PaintFxAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Paint Fx Mode is admin only. Ask an admin for help."); + return; + } + + %client.ndSetMode(NDM_FillColorProgress); + + if(%client.currentFxColor < 7) + %client.ndSelection.startFillColor(1, %client.currentFxColor); + else + %client.ndSelection.startFillColor(2, %client.currentFxColor - 7); +} + +//Cancel Brick +function NDM_FillColor::onCancelBrick(%this, %client) +{ + %client.ndSetMode(%client.ndLastSelectMode); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_FillColor::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + %title = "Paint Mode (\c3" @ %count @ "\c6 Brick" @ (%count > 1 ? "s)" : ")"); + + if(%client.currentFxColor !$= "") + { + switch(%client.currentFxColor) + { + case 0: %color = "\c3Fx - None"; + case 1: %color = "\c3Fx - Pearl"; + case 2: %color = "\c3Fx - Chrome"; + case 3: %color = "\c3Fx - Glow"; + case 4: %color = "\c3Fx - Blink"; + case 5: %color = "\c3Fx - Swirl"; + case 6: %color = "\c3Fx - Rainbow"; + case 7: %color = "\c3Fx - Stable"; + case 8: %color = "\c3Fx - Undulo"; + } + } + else + { + %color = "" @ ndGetPaintColorCode(%client.currentColor) @ "|||||\c3"; + + %alpha = mFloor(100 * getWord(getColorIdTable(%client.currentColor), 3)); + + if(%alpha != 100) + %color = %color SPC %alpha @ "%"; + } + + + %l0 = "Select paint can to chose color"; + %l1 = "Color: " @ %color; + + %r0 = "[Plant Brick]: Paint bricks"; + %r1 = "[Cancel Brick]: Exit mode"; + + return ndFormatMessage(%title, %l0, %r0, %l1, %r1, %l2); +} diff --git a/classes/server/duplimode/fillcolorprogress.cs b/classes/server/duplimode/fillcolorprogress.cs new file mode 100644 index 0000000..bfac7e7 --- /dev/null +++ b/classes/server/duplimode/fillcolorprogress.cs @@ -0,0 +1,41 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_FillColorProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_FillColorProgress::onCancelBrick(%this, %client) +{ + %client.ndSelection.cancelFillColor(); + %client.ndSetMode(NDM_FillColor); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_FillColorProgress::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + %percent = mFloor(%client.ndSelection.paintIndex * 100 / %count); + + %title = "Painting... (\c3" @ %percent @ "%\c6)"; + %l0 = "[Cancel Brick]: Cancel painting"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/loadprogress.cs b/classes/server/duplimode/loadprogress.cs new file mode 100644 index 0000000..80c2337 --- /dev/null +++ b/classes/server/duplimode/loadprogress.cs @@ -0,0 +1,76 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Switch to this mode +function NDM_LoadProgress::onStartMode(%this, %client, %lastMode) +{ + //Prepare selection to load data into + if(isObject(%client.ndSelection)) + %client.ndSelection.deleteData(); + else + %client.ndSelection = ND_Selection(%client); +} + +//Kill this mode +function NDM_LoadProgress::onKillMode(%this, %client) +{ + //Destroy selection + %client.ndSelection.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_LoadProgress::onCancelBrick(%this, %client) +{ + %client.ndSelection.cancelLoading(); + %client.ndSelection.delete(); + + %client.ndSetMode(%client.ndLastSelectMode); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_LoadProgress::getBottomPrint(%this, %client) +{ + if(%client.ndSelection.loadStage == 0) + { + %count = %client.ndSelection.loadExpectedBrickCount; + + if(%count != 0) + { + %percent = mFloor(%client.ndSelection.brickCount * 100 / %count); + + %title = "Loading Bricks... (\c3" @ %percent @ "%\c6)"; + } + else + %title = "Loading Bricks... (\c3" @ %client.ndSelection.brickCount @ "\c6 Bricks)"; + } + else + { + %count = %client.ndSelection.loadExpectedConnectionCount; + + if(%count != 0) + { + %percent = mFloor(%client.ndSelection.connectionCount * 100 / %count); + + %title = "Loading Connections... (\c3" @ %percent @ "%\c6)"; + } + else + %title = "Loading Connections... (\c3" @ %client.ndSelection.connectionCount @ "\c6 Connections)"; + } + + %l0 = "[Cancel Brick]: Cancel loading"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/plantcopy.cs b/classes/server/duplimode/plantcopy.cs new file mode 100644 index 0000000..14d5568 --- /dev/null +++ b/classes/server/duplimode/plantcopy.cs @@ -0,0 +1,268 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Switch to this mode +function NDM_PlantCopy::onStartMode(%this, %client, %lastMode) +{ + if(%lastMode == $NDM::StackSelect + || %lastMode == $NDM::BoxSelect + || %lastMode == $NDM::CutProgress + || %lastMode == $NDM::LoadProgress) + { + %client.ndSelection.spawnGhostBricks(%client.ndSelection.rootPosition, 0); + %client.ndSelection.angleIdReference = getAngleIDFromPlayer(%client.getControlObject()); + } + + %client.ndUpdateBottomPrint(); +} + +//Switch away from this mode +function NDM_PlantCopy::onChangeMode(%this, %client, %nextMode) +{ + if(%nextMode == $NDM::StackSelect || %nextMode == $NDM::BoxSelect) + { + %client.ndSelection.deleteData(); + } +} + +//Kill this mode +function NDM_PlantCopy::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); +} + + + +//Duplicator image callbacks +/////////////////////////////////////////////////////////////////////////// + +//Selecting an object with the duplicator +function NDM_PlantCopy::onSelectObject(%this, %client, %obj, %pos, %normal) +{ + %this.moveBricksTo(%client, %pos, %normal); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Prev Seat +function NDM_PlantCopy::onPrevSeat(%this, %client) +{ + %client.ndPivot = !%client.ndPivot; + %client.ndUpdateBottomPrint(); + + if($Pref::Server::ND::PlayMenuSounds) + %client.play2d(%client.ndPivot ? lightOnSound : lightOffSound); +} + +//Shift Brick +function NDM_PlantCopy::onShiftBrick(%this, %client, %x, %y, %z) +{ + switch(getAngleIDFromPlayer(%client.getControlObject())) + { + case 0: %newX = %x; %newY = %y; + case 1: %newX = -%y; %newY = %x; + case 2: %newX = -%x; %newY = -%y; + case 3: %newX = %y; %newY = -%x; + } + + %client.ndSelection.shiftGhostBricks(%newX / 2 SPC %newY / 2 SPC %z / 5); +} + +//Super Shift Brick +function NDM_PlantCopy::onSuperShiftBrick(%this, %client, %x, %y, %z) +{ + switch(getAngleIDFromPlayer(%client.getControlObject())) + { + case 0: %newX = %x; %newY = %y; + case 1: %newX = -%y; %newY = %x; + case 2: %newX = -%x; %newY = -%y; + case 3: %newX = %y; %newY = -%x; + } + + if(%client.ndPivot) + %box = %client.ndSelection.getGhostWorldBox(); + else + %box = %client.ndSelection.ghostGroup.getObject(0).getWorldBox(); + + %newX *= (getWord(%box, 3) - getWord(%box, 0)); + %newy *= (getWord(%box, 4) - getWord(%box, 1)); + %z *= (getWord(%box, 5) - getWord(%box, 2)); + + %client.ndSelection.shiftGhostBricks(%newX SPC %newY SPC %z); +} + +//Rotate Brick +function NDM_PlantCopy::onRotateBrick(%this, %client, %direction) +{ + %client.ndSelection.rotateGhostBricks(%direction, %client.ndPivot); +} + +//Plant Brick +function NDM_PlantCopy::onPlantBrick(%this, %client) +{ + //Check force plant + if(%client.ndForcePlant) + { + if($Pref::Server::ND::FloatAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Force Plant has been disabled because it is admin only. Ask an admin for help."); + %client.ndForcePlant = false; + } + } + + %this.conditionalPlant(%client, %client.ndForcePlant); +} + +//Cancel Brick +function NDM_PlantCopy::onCancelBrick(%this, %client) +{ + if(%client.ndEquipped) + %client.ndSetMode(%client.ndLastSelectMode); + else + %client.ndKillMode(); +} + +//Paste Selection +function NDM_PlantCopy::onPaste(%this, %client) +{ + %this.onPlantBrick(%client); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_PlantCopy::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + + %size = vectorSub(%client.ndSelection.maxSize, %client.ndSelection.minSize); + + %x = mFloor(getWord(%size, 0) * 2); + %y = mFloor(getWord(%size, 1) * 2); + %z = mFloor(getWord(%size, 2) * 5); + + if(%count == 1) + %title = "Plant Mode (\c31\c6 Brick)"; + else if(%count <= $Pref::Server::ND::MaxGhostBricks) + %title = "Plant Mode (\c3" @ %count @ "\c6 Bricks)"; + else + %title = "Plant Mode (\c3" @ %count @ "\c6 Bricks, \c3" @ mFloor($Pref::Server::ND::MaxGhostBricks * 100 / %count) @ "%\c6 Ghosted)"; + + %l0 = "Pivot: \c3" @ (%client.ndPivot ? "Whole Selection" : "Start Brick") @ "\c6 [Prev Seat]"; + + if(isObject(%client.ndSelection.targetGroup)) + %l1 = "Planting as: \c3" @ %client.ndSelection.targetGroup.name; + else + %l1 = "Size: \c3" @ %x @ "\c6 x \c3" @ %y @ "\c6 x \c3" @ %z @ "\c6 Plates"; + + %r0 = "Use normal ghost brick controls"; + %r1 = "[Cancel Brick] to exit plant mode"; + + return ndFormatMessage(%title, %l0, %r0, %l1, %r1); +} + + + +//Functions +/////////////////////////////////////////////////////////////////////////// + +//Move the bricks to a specific location, like with the brick tool +function NDM_PlantCopy::moveBricksTo(%his, %client, %pos, %normal) +{ + //Get half size of world box for offset + if(%client.ndPivot) + %box = %client.ndSelection.getGhostWorldBox(); + else + %box = %client.ndSelection.ghostGroup.getObject(0).getWorldBox(); + + %halfSize = vectorScale(vectorSub(getWords(%box, 3, 5), getWords(%box, 0, 2)), 0.5); + + //Point offset in correct direction based on normal + %offX = getWord(%halfSize, 0) * mFloatLength(getWord(%normal, 0), 0); + %offY = getWord(%halfSize, 1) * mFloatLength(getWord(%normal, 1), 0); + %offZ = getWord(%halfSize, 2) * mFloatLength(getWord(%normal, 2), 0); + %offset = %offX SPC %offY SPC %offZ; + + //Get shift vector + %pos = vectorSub(vectorAdd(%pos, %offset), %client.ndSelection.ghostPosition); + + if(%client.ndPivot) + { + %toCenter = %client.ndSelection.rootToCenter; + + //Apply mirror + if(%client.ndSelection.ghostMirrorX) + %toCenter = -firstWord(%toCenter) SPC restWords(%toCenter); + else if(%client.ndSelection.ghostMirrorY) + %toCenter = getWord(%toCenter, 0) SPC -getWord(%toCenter, 1) SPC getWord(%toCenter, 2); + + if(%client.ndSelection.ghostMirrorZ) + %toCenter = getWord(%toCenter, 0) SPC getWord(%toCenter, 1) SPC -getWord(%toCenter, 2); + + %pos = vectorSub(%pos, ndRotateVector(%toCenter, %client.ndSelection.ghostAngleID)); + } + + %client.ndSelection.shiftGhostBricks(%pos); + + //Offset required for New Brick Tool to display the tracer shape correctly + if(%client.ndPivot) + return vectorSub(%client.ndSelection.getGhostCenter(), %offset); + else + return vectorSub(%client.ndSelection.ghostGroup.getObject(0).getWorldBoxCenter(), %offset); +} + +//Check time limit and attempt to plant bricks +function NDM_PlantCopy::conditionalPlant(%this, %client, %force) +{ + //Check timeout + if(!%client.isAdmin && %client.ndLastPlantTime + ($Pref::Server::ND::PlantTimeoutMS / 1000) > $Sim::Time) + { + %remain = mCeil(%client.ndLastPlantTime + ($Pref::Server::ND::PlantTimeoutMS / 1000) - $Sim::Time); + + if(%remain != 1) + %s = "s"; + + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6You need to wait\c3 " @ %remain @ "\c6 second" @ %s @ " before planting again!", 5); + return; + } + + //Check too far distance + %offset = vectorSub(%client.ndSelection.getGhostCenter(), %client.getControlObject().position); + + if(vectorLen(%offset) > $Pref::Server::TooFarDistance) + { + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6You can't plant so far away!", 5); + return; + } + + //Validate target group + if(isObject(%client.ndSelection.targetGroup) && + getTrustLevel(%client, %client.ndSelection.targetGroup) < 1 && + (!%client.isAdmin || !$Pref::Server::ND::AdminTrustBypass2)) + { + messageClient(%client, '', "\c6You need build trust with \c3" + @ %client.ndSelection.targetGroup.name @ "\c6 to plant bricks in their group."); + + return; + } + + %client.ndLastPlantTime = $Sim::Time; + + %pos = %client.ndSelection.ghostPosition; + %ang = %client.ndSelection.ghostAngleID; + + %client.ndSetMode(NDM_PlantCopyProgress); + %client.ndSelection.startPlant(%pos, %ang, %force); +} diff --git a/classes/server/duplimode/plantcopyprogress.cs b/classes/server/duplimode/plantcopyprogress.cs new file mode 100644 index 0000000..8e3bd85 --- /dev/null +++ b/classes/server/duplimode/plantcopyprogress.cs @@ -0,0 +1,62 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_PlantCopyProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_PlantCopyProgress::onCancelBrick(%this, %client) +{ + commandToClient(%client, 'centerPrint', "\c6Planting canceled!", 4); + + %client.ndSelection.cancelPlanting(); + %client.ndSetMode(NDM_PlantCopy); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_PlantCopyProgress::getBottomPrint(%this, %client) +{ + %qIndex = %client.ndSelection.plantQueueIndex; + %qCount = %client.ndSelection.plantQueueCount; + + %count = %client.ndSelection.brickCount; + %planted = %client.ndSelection.plantSuccessCount; + + if(%qIndex == %qCount) + { + //Searching for a brick + %pIndex = %client.ndSelection.plantSearchIndex; + %percent = mFloor(%client.ndSelection.plantSearchIndex * 100 / %count); + + %title = "Finding Next Brick... (\c3" @ %percent @ "%\c6, \c3" @ %planted @ "\c6 planted)"; + } + else + { + //Planting bricks + %failed = %client.ndSelection.plantTrustFailCount + %client.ndSelection.plantBlockedFailCount; + %percent = mFloor(%planted * 100 / %count); + + %title = "Planting... (\c3" @ %percent @ "%\c6, \c3" @ %failed @ "\c6 failed)"; + } + + %l0 = "[Cancel Brick]: Cancel planting"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/saveprogress.cs b/classes/server/duplimode/saveprogress.cs new file mode 100644 index 0000000..dc6d68c --- /dev/null +++ b/classes/server/duplimode/saveprogress.cs @@ -0,0 +1,43 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_SaveProgress::onKillMode(%this, %client) +{ + //Destroy selection + %client.ndSelection.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_SaveProgress::onCancelBrick(%this, %client) +{ + %client.ndSelection.cancelSaving(); + %client.ndSetMode(NDM_PlantCopy); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_SaveProgress::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + %index = %client.ndSelection.saveIndex / 2 + %client.ndSelection.saveStage * %count / 2; + + %percent = mFloor(%index * 100 / %count); + + %title = "Saving Selection... (\c3" @ %percent @ "%\c6)"; + %l0 = "[Cancel Brick]: Cancel saving"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/stackselect.cs b/classes/server/duplimode/stackselect.cs new file mode 100644 index 0000000..df49ace --- /dev/null +++ b/classes/server/duplimode/stackselect.cs @@ -0,0 +1,210 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Switch to this mode +function NDM_StackSelect::onStartMode(%this, %client, %lastMode) +{ + %client.ndLastSelectMode = %this; + %client.ndUpdateBottomPrint(); +} + +//Switch away from this mode +function NDM_StackSelect::onChangeMode(%this, %client, %nextMode) +{ + if(%nextMode == $NDM::FillColor + || %nextMode == $NDM::PlantCopy + || %nextMode == $NDM::WrenchProgress) + { + //Start de-highlighting the bricks + %client.ndSelection.deHighlight(); + } + + //The transition to box select mode will be + //handled in NDM_BoxSelect::onStartMode +} + +//Kill this mode +function NDM_StackSelect::onKillMode(%this, %client) +{ + //Destroy selection + if(isObject(%client.ndSelection)) + %client.ndSelection.delete(); +} + + + +//Duplicator image callbacks +/////////////////////////////////////////////////////////////////////////// + +//Selecting an object with the duplicator +function NDM_StackSelect::onSelectObject(%this, %client, %obj, %pos, %normal) +{ + if((%obj.getType() & $TypeMasks::FxBrickAlwaysObjectType) == 0) + return; + + //Check timeout + if(!%client.isAdmin && %client.ndLastSelectTime + ($Pref::Server::ND::SelectTimeoutMS / 1000) > $Sim::Time) + { + %remain = mCeil(%client.ndLastSelectTime + ($Pref::Server::ND::SelectTimeoutMS / 1000) - $Sim::Time); + + if(%remain != 1) + %s = "s"; + + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6You need to wait\c3 " @ + %remain @ "\c6 second" @ %s @ " before selecting again!", 5); + + return; + } + + %client.ndLastSelectTime = $Sim::Time; + + if(!ndTrustCheckMessage(%obj, %client)) + return; + + //Prepare selection to copy the bricks + if(!isObject(%client.ndSelection)) + %client.ndSelection = ND_Selection(%client); + + //Start selection + %client.ndSetMode(NDM_StackSelectProgress); + + if(%client.ndMultiSelect) + %client.ndSelection.startStackSelectionAdditive(%obj, %client.ndDirection, %client.ndLimited); + else + %client.ndSelection.startStackSelection(%obj, %client.ndDirection, %client.ndLimited); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Light key +function NDM_StackSelect::onLight(%this, %client) +{ + if($Pref::Server::ND::PlayMenuSounds) + %client.play2d(lightOnSound); + + %client.ndSetMode(NDM_BoxSelect); +} + +//Next Seat +function NDM_StackSelect::onNextSeat(%this, %client) +{ + %client.ndDirection = !%client.ndDirection; + %client.ndUpdateBottomPrint(); + + if($Pref::Server::ND::PlayMenuSounds) + %client.play2d(%client.ndDirection ? lightOnSound : lightOffSound); +} + +//Prev Seat +function NDM_StackSelect::onPrevSeat(%this, %client) +{ + %client.ndLimited = !%client.ndLimited; + %client.ndUpdateBottomPrint(); + + if($Pref::Server::ND::PlayMenuSounds) + %client.play2d(%client.ndLimited ? lightOnSound : lightOffSound); +} + +//Shift Brick +function NDM_StackSelect::onShiftBrick(%this, %client, %x, %y, %z) +{ + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + return; + + //Change to plant mode and apply the shift + %client.ndSetMode(NDM_PlantCopy); + NDM_PlantCopy.onShiftBrick(%client, %x, %y, %z); +} + +//Super Shift Brick +function NDM_StackSelect::onSuperShiftBrick(%this, %client, %x, %y, %z) +{ + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + return; + + //Change to plant mode and apply the shift + %client.ndSetMode(NDM_PlantCopy); + NDM_PlantCopy.onSuperShiftBrick(%client, %x, %y, %z); +} + +//Rotate Brick +function NDM_StackSelect::onRotateBrick(%this, %client, %dir) +{ + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + return; + + //Change to plant mode and apply the shift + %client.ndSetMode(NDM_PlantCopy); + NDM_PlantCopy.onRotateBrick(%client, %dir); +} + +//Plant Brick +function NDM_StackSelect::onPlantBrick(%this, %client) +{ + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + return; + + %client.ndSetMode(NDM_PlantCopy); +} + +//Cancel Brick +function NDM_StackSelect::onCancelBrick(%this, %client) +{ + if(isObject(%client.ndSelection)) + %client.ndSelection.deleteData(); + + %client.ndUpdateBottomPrint(); +} + +//Copy Selection +function NDM_StackSelect::onCopy(%this, %client) +{ + %this.onPlantBrick(%client); +} + +//Cut Selection +function NDM_StackSelect::onCut(%this, %client) +{ + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + return; + + %client.ndSetMode(NDM_CutProgress); + %client.ndSelection.startCutting(); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_StackSelect::getBottomPrint(%this, %client) +{ + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + { + %title = "Selection Mode"; + %r0 = "Click Brick: Select stack " @ (%client.ndDirection ? "up" : "down"); + %r1 = ""; + } + else + { + %count = %client.ndSelection.brickCount; + + %title = "Selection Mode (\c3" @ %count @ "\c6 Brick" @ (%count > 1 ? "s)" : ")"); + %r0 = "Ctrl-Click Brick: Multiselect"; + %r1 = "[Plant Brick]: Duplicate"; + } + + %l0 = "Type: \c3" @ (%client.ndMultiSelect ? "Multi-" : "") @ "Stack \c6[Light]"; + %l1 = "Limited: " @ (%client.ndLimited ? "\c3Yes" : "\c0No") @ " \c6[Prev Seat]"; + %l2 = "Direction: \c3" @ (%client.ndDirection ? "Up" : "Down") @ " \c6[Next Seat]"; + + return ndFormatMessage(%title, %l0, %r0, %l1, %r1, %l2); +} diff --git a/classes/server/duplimode/stackselectprogress.cs b/classes/server/duplimode/stackselectprogress.cs new file mode 100644 index 0000000..956abc2 --- /dev/null +++ b/classes/server/duplimode/stackselectprogress.cs @@ -0,0 +1,43 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_StackSelectProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_StackSelectProgress::onCancelBrick(%this, %client) +{ + commandToClient(%client, 'centerPrint', "\c6Selection canceled!", 4); + + %client.ndSelection.cancelStackSelection(); + %client.ndSetMode(NDM_StackSelect); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_StackSelectProgress::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + %qCount = %client.ndSelection.queueCount - %count; + + %title = "Selecting... (\c3" @ %count @ "\c6 Bricks, \c3" @ %qCount @ "\c6 in Queue)"; + %l0 = "[Cancel Brick]: Cancel selection"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/supercutprogress.cs b/classes/server/duplimode/supercutprogress.cs new file mode 100644 index 0000000..554afb2 --- /dev/null +++ b/classes/server/duplimode/supercutprogress.cs @@ -0,0 +1,50 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_SuperCutProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); + + //Remove selection box + %client.ndSelectionBox.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_SuperCutProgress::onCancelBrick(%this, %client) +{ + commandToClient(%client, 'centerPrint', "\c6Supercut canceled!", 4); + + %client.ndSelection.cancelSuperCut(); + %client.ndSetMode(NDM_BoxSelect); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_SuperCutProgress::getBottomPrint(%this, %client) +{ + %curr = %client.ndSelection.currChunk + 1; + %num = %client.ndSelection.numChunks; + + %percent = mFloor(%curr * 100 / %num); + %deleted = %client.ndSelection.superCutCount; + %planted = %client.ndSelection.superCutPlacedCount; + + %title = "Supercut in progress... (\c3" @ %percent @ "%\c6, \c3" @ %deleted @ "\c6 deleted, \c3" @ %planted @ "\c6 planted)"; + %l0 = "[Cancel Brick]: Cancel supercut"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/duplimode/wrenchprogress.cs b/classes/server/duplimode/wrenchprogress.cs new file mode 100644 index 0000000..b5fa6c1 --- /dev/null +++ b/classes/server/duplimode/wrenchprogress.cs @@ -0,0 +1,45 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Changing modes +/////////////////////////////////////////////////////////////////////////// + +//Kill this mode +function NDM_WrenchProgress::onKillMode(%this, %client) +{ + //Destroy the selection + %client.ndSelection.delete(); + + //Remove the selection box + if(isObject(%client.ndSelectionBox)) + %client.ndSelectionBox.delete(); +} + + + +//Generic inputs +/////////////////////////////////////////////////////////////////////////// + +//Cancel Brick +function NDM_WrenchProgress::onCancelBrick(%this, %client) +{ + %client.ndSelection.cancelFillWrench(); + %client.ndSetMode(%client.ndLastSelectMode); +} + + + +//Interface +/////////////////////////////////////////////////////////////////////////// + +//Create bottomprint for client +function NDM_WrenchProgress::getBottomPrint(%this, %client) +{ + %count = %client.ndSelection.brickCount; + %percent = mFloor(%client.ndSelection.wrenchIndex * 100 / %count); + + %title = "Applying... (\c3" @ %percent @ "%\c6)"; + %l0 = "[Cancel Brick]: Cancel"; + + return ndFormatMessage(%title, %l0); +} diff --git a/classes/server/ghostgroup.cs b/classes/server/ghostgroup.cs new file mode 100644 index 0000000..85dc1f9 --- /dev/null +++ b/classes/server/ghostgroup.cs @@ -0,0 +1,38 @@ +// Deletes large numbers of ghost bricks without causing lag. +// ------------------------------------------------------------------- + +//Create a new ghost group +function ND_GhostGroup() +{ + ND_ServerGroup.add( + %this = new ScriptGroup(ND_GhostGroup) + ); + + return %this; +} + +//Delete some of the bricks in this group +function ND_GhostGroup::tickDelete(%this) +{ + %max = $Pref::Server::ND::ProcessPerTick; + %bricks = getBrickCount(); + + //Deleting objects causes increasing lag with more bricks in total + if(%bricks > 450000) + %max /= 6; + else if(%bricks > 300000) + %max /= 4; + else if(%bricks > 150000) + %max /= 2; + + if(%this.getCount() <= %max) + { + %this.delete(); + return; + } + + for(%i = 0; %i < %max; %i++) + %this.getObject(0).delete(); + + %this.schedule(30, tickDelete); +} diff --git a/classes/server/highlightbox.cs b/classes/server/highlightbox.cs new file mode 100644 index 0000000..e8351bd --- /dev/null +++ b/classes/server/highlightbox.cs @@ -0,0 +1,112 @@ +// Resizable highlight box to visualize the size of the selection. +// ------------------------------------------------------------------- + +//Create a new highlight box +function ND_HighlightBox() +{ + ND_ServerGroup.add( + %this = new ScriptObject(ND_HighlightBox) + ); + + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i] = new StaticShape(){datablock = ND_SelectionBoxBorder;}; + %this.border_y[%i] = new StaticShape(){datablock = ND_SelectionBoxBorder;}; + %this.border_z[%i] = new StaticShape(){datablock = ND_SelectionBoxBorder;}; + + %this.border_x[%i].setScopeAlways(); + %this.border_y[%i].setScopeAlways(); + %this.border_z[%i].setScopeAlways(); + } + + %this.color = "1 0.84 0 0.99"; + %this.applyColors(); + + return %this; +} + +//Destroy static shapes when highlight box is removed +function ND_HighlightBox::onRemove(%this) +{ + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i].delete(); + %this.border_y[%i].delete(); + %this.border_z[%i].delete(); + } +} + +//Apply color changes to the highlight box +function ND_HighlightBox::applyColors(%this) +{ + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i].setNodeColor("ALL", %this.color); + %this.border_y[%i].setNodeColor("ALL", %this.color); + %this.border_z[%i].setNodeColor("ALL", %this.color); + } +} + +//Return current size of highlight box +function ND_HighlightBox::getSize(%this) +{ + return %this.point1 SPC %this.point2; +} + +//Resize the highlight box +function ND_HighlightBox::setSize(%this, %point1, %point2) +{ + if(getWordCount(%point1) == 6) + { + %point2 = getWords(%point1, 3, 5); + %point1 = getWords(%point1, 0, 2); + } + + %this.point1 = %point1; + %this.point2 = %point2; + + %x1 = getWord(%point1, 0); + %y1 = getWord(%point1, 1); + %z1 = getWord(%point1, 2); + + %x2 = getWord(%point2, 0); + %y2 = getWord(%point2, 1); + %z2 = getWord(%point2, 2); + + %len_x = %x2 - %x1; + %len_y = %y2 - %y1; + %len_z = %z2 - %z1; + + %center_x = (%x1 + %x2) / 2; + %center_y = (%y1 + %y2) / 2; + %center_z = (%z1 + %z2) / 2; + + %rot_x = "0 1 0 1.57079"; + %rot_y = "1 0 0 1.57079"; + %rot_z = "0 0 1 0"; + + %this.border_x0.setTransform(%center_x SPC %y1 SPC %z1 SPC %rot_x); + %this.border_x1.setTransform(%center_x SPC %y2 SPC %z1 SPC %rot_x); + %this.border_x2.setTransform(%center_x SPC %y2 SPC %z2 SPC %rot_x); + %this.border_x3.setTransform(%center_x SPC %y1 SPC %z2 SPC %rot_x); + + %this.border_y0.setTransform(%x1 SPC %center_y SPC %z1 SPC %rot_y); + %this.border_y1.setTransform(%x2 SPC %center_y SPC %z1 SPC %rot_y); + %this.border_y2.setTransform(%x2 SPC %center_y SPC %z2 SPC %rot_y); + %this.border_y3.setTransform(%x1 SPC %center_y SPC %z2 SPC %rot_y); + + %this.border_z0.setTransform(%x1 SPC %y1 SPC %center_z SPC %rot_z); + %this.border_z1.setTransform(%x2 SPC %y1 SPC %center_z SPC %rot_z); + %this.border_z2.setTransform(%x2 SPC %y2 SPC %center_z SPC %rot_z); + %this.border_z3.setTransform(%x1 SPC %y2 SPC %center_z SPC %rot_z); + + %maxLen = getMax(getMax(%len_x, %len_y), %len_z); + %width = (7 / 1280) * %maxLen + 1; + + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i].setScale(%width SPC %width SPC %len_x + %width * 0.05); + %this.border_y[%i].setScale(%width SPC %width SPC %len_y + %width * 0.05); + %this.border_z[%i].setScale(%width SPC %width SPC %len_z + %width * 0.05); + } +} diff --git a/classes/server/selection.cs b/classes/server/selection.cs new file mode 100644 index 0000000..1b8d7d7 --- /dev/null +++ b/classes/server/selection.cs @@ -0,0 +1,4494 @@ +// This file is way too big. Fix later... +// ------------------------------------------------------------------- + +//Selection data arrays $NS[obj, type{, ...}] +// $NS[%s, "B", %i ] - Brick object +// $NS[%s, "I", %b ] - Index of brick in array +// $NS[%s, "N", %i ] - Number of connected bricks +// $NS[%s, "C", %i, %j] - Index of connected brick + +// $NS[%s, "D", %i] - Datablock +// $NS[%s, "P", %i] - Position +// $NS[%s, "R", %i] - Rotation + +// $NS[%s, "NT", %i] - Brick name +// $NS[%s, "HN", %n] - Name exists in selection +// $NS[%s, "PR", %i] - Print + +// $NS[%s, "CO", %i] - Color id +// $NS[%s, "CF", %i] - Color Fx id +// $NS[%s, "SF", %i] - Shape Fx id + +// $NS[%s, "NRC", %i] - No ray casting +// $NS[%s, "NR", %i] - No rendering +// $NS[%s, "NC", %i] - No colliding + +// $NS[%s, "LD", %i] - Light datablock + +// $NS[%s, "ED", %i] - Emitter datablock +// $NS[%s, "ER", %i] - Emitter rotation + +// $NS[%s, "ID", %i] - Item datablock +// $NS[%s, "IP", %i] - Item position +// $NS[%s, "IR", %i] - Item rotation +// $NS[%s, "IT", %i] - Item respawn time + +// $NS[%s, "VD", %i] - Vehicle datablock +// $NS[%s, "VC", %i] - Vehicle color + +// $NS[%s, "MD", %i] - Music datablock + + +// $NS[%s, "EN", %i] - Number of events on the brick + +// $NS[%s, "EE", %i, %j] - Whether event is enabled +// $NS[%s, "ED", %i, %j] - Event delay + +// $NS[%s, "EI", %i, %j] - Event input name +// $NS[%s, "EII", %i, %j] - Event input idx +// $NS[%s, "EO", %i, %j] - Event output name +// $NS[%s, "EOI", %i, %j] - Event output idx +// $NS[%s, "EOC", %i, %j] - Event output append client + +// $NS[%s, "ET", %i, %j] - Event target name +// $NS[%s, "ETI", %i, %j] - Event target idx +// $NS[%s, "ENT", %i, %j] - Event brick named target + +// $NS[%s, "EP", %i, %j, %k] - Event output parameter + + +//Mirror error lists $NS[client, type{, ...}] +// $NS[%c, "MXC", ] - Count of mirror errors on x +// $NS[%c, "MXE", %i] - Error datablock +// $NS[%c, "MXK", %d] - Index of datablock in list + +// $NS[%c, "MZC", ] - Count of mirror errors on z +// $NS[%c, "MZE", %i] - Error datablock +// $NS[%c, "MZK", %d] - Index of datablock in list + + + +//General +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Create selection +function ND_Selection(%client) +{ + ND_ServerGroup.add( + %this = new ScriptObject(ND_Selection) + { + client = %client; + } + ); + + return %this; +} + +//Delete all the selection variables, allowing re-use of object +function ND_Selection::deleteData(%this) +{ + //If count isn't at least 1, assume there is no data + if(%this.queueCount >= 1 || %this.brickCount >= 1) + { + //Variables follow the pattern $NS[object]_[type]_[...], allowing a single iteration to remove all + deleteVariables("$NS" @ %this @ "_*"); + } + + %this.rootPosition = "0 0 0"; + %this.queueCount = 0; + %this.brickCount = 0; + + %this.targetGroup = ""; + %this.targetBlid = ""; + + %this.deHighlight(); + %this.deleteHighlightBox(); + %this.deleteGhostBricks(); + + if(isObject(%this.saveFile)) + %this.saveFile.delete(); +} + +//Remove data when selection is deleted +function ND_Selection::onRemove(%this) +{ + %this.deleteData(); + + if(isEventPending(%this.plantSchedule)) + %this.cancelPlanting(); +} + + + +//Stack Selection +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Begin stack selection +function ND_Selection::startStackSelection(%this, %brick, %direction, %limited) +{ + //Clear previous selection + %this.deleteData(); + + //Create new highlight group + %highlightGroup = ndNewHighlightGroup(); + + %this.brickLimitReached = false; + + if(%this.client.isAdmin) + %brickLimit = $Pref::Server::ND::MaxBricksAdmin; + else + %brickLimit = $Pref::Server::ND::MaxBricksPlayer; + + //Root position is position of the first selected brick + %this.rootPosition = %brick.getPosition(); + + //Process first brick + %queueCount = 1; + %brickCount = 1; + + $NS[%this, "B", 0] = %brick; + $NS[%this, "I", %brick] = 0; + + %this.recordBrickData(0); + ndHighlightBrick(%highlightGroup, %brick); + + //Variables for trust checks + %admin = %this.client.isAdmin; + %group = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + + //Add bricks connected to the first brick to queue (do not register connections yet) + if(%direction == 1) + { + //Set lower height limit + %heightLimit = %this.minZ - 0.01; + %upCount = %brick.getNumUpBricks(); + + for(%i = 0; %i < %upCount; %i++) + { + %nextBrick = %brick.getUpBrick(%i); + + //If the brick is not in the list yet, add it to the queue + if($NS[%this, "I", %nextBrick] $= "") + { + if(%queueCount >= %brickLimit) + continue; + + //Check trust + if(!ndTrustCheckSelect(%nextBrick, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + $NS[%this, "B", %queueCount] = %nextBrick; + $NS[%this, "I", %nextBrick] = %queueCount; + %queueCount++; + } + } + } + else + { + //Set upper height limit + %heightLimit = %this.maxZ + 0.01; + %downCount = %brick.getNumDownBricks(); + + for(%i = 0; %i < %downCount; %i++) + { + %nextBrick = %brick.getDownBrick(%i); + + //If the brick is not in the list yet, add it to the queue + if($NS[%this, "I", %nextBrick] $= "") + { + if(%queueCount >= %brickLimit) + continue; + + //Check trust + if(!ndTrustCheckSelect(%nextBrick, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + $NS[%this, "B", %queueCount] = %nextBrick; + $NS[%this, "I", %nextBrick] = %queueCount; + %queueCount++; + } + } + } + + //Save number of connections + %this.maxConnections = 0; + %this.connectionCount = 0; + + %this.trustFailCount = %trustFailCount; + %this.highlightGroup = %highlightGroup; + %this.queueCount = %queueCount; + %this.brickCount = %brickCount; + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + //First selection tick + %this.selectionStart = 1; + + if(%queueCount > %brickCount) + %this.tickStackSelection(%direction, %limited, %heightLimit, %brickLimit); + else + %this.finishStackSelection(); +} + +//Begin stack selection (multiselect) +function ND_Selection::startStackSelectionAdditive(%this, %brick, %direction, %limited) +{ + //If we have no bricks, start normal stack selection + if(%this.brickCount < 1) + { + %this.startStackSelection(%brick, %direction, %limited); + return; + } + + //If we already reched the limit, don't even try + if(%this.brickLimitReached) + { + %this.finishStackSelection(); + return; + } + + %highlightGroup = %this.highlightGroup; + + if(%this.client.isAdmin) + %brickLimit = $Pref::Server::ND::MaxBricksAdmin; + else + %brickLimit = $Pref::Server::ND::MaxBricksPlayer; + + %queueCount = %this.queueCount; + %brickCount = %this.brickCount; + + //If the brick is not part of the selection yet, process it + if($NS[%this, "I", %brick] $= "") + { + $NS[%this, "B", %queueCount] = %brick; + $NS[%this, "I", %brick] = %queueCount; + + %this.recordBrickData(%queueCount); + ndHighlightBrick(%highlightGroup, %brick); + + %brickIsNew = true; + %brickIndex = %queueCount; + %conns = 0; + + %queueCount++; + %brickCount++; + } + + //Variables for trust checks + %admin = %this.client.isAdmin; + %group = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + + //Add bricks connected to the first brick to queue (do not register connections yet) + if(%direction == 1) + { + //Set lower height limit + %heightLimit = getWord(%brick.getWorldBox(), 2) - 0.01; + } + else + { + //Set upper height limit + %heightLimit = getWord(%brick.getWorldBox(), 5) + 0.01; + } + + //Process all up bricks + %upCount = %brick.getNumUpBricks(); + + for(%i = 0; %i < %upCount; %i++) + { + %nextBrick = %brick.getUpBrick(%i); + + //If the brick is not in the list yet, add it to the queue + %nId = $NS[%this, "I", %nextBrick]; + + if(%nId $= "") + { + //Don't add up bricks if we're searching down + if(%direction != 1) + continue; + + if(%queueCount >= %brickLimit) + continue; + + //Check trust + if(!ndTrustCheckSelect(%nextBrick, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + $NS[%this, "B", %queueCount] = %nextBrick; + $NS[%this, "I", %nextBrick] = %queueCount; + %queueCount++; + } + else if(%brickIsNew) + { + //If this brick already exists, we have to add the connection now + //(Start brick won't be processed again unlike the others) + $NS[%this, "C", %brickIndex, %conns] = %nId; + %conns++; + + %ci = $NS[%this, "N", %nId]++; + $NS[%this, "C", %nId, %ci - 1] = %brickIndex; + + if(%ci > %this.maxConnections) + %this.maxConnections = %ci; + + %this.connectionCount++; + } + } + + //Process all down bricks + %downCount = %brick.getNumDownBricks(); + + for(%i = 0; %i < %downCount; %i++) + { + %nextBrick = %brick.getDownBrick(%i); + + //If the brick is not in the list yet, add it to the queue + %nId = $NS[%this, "I", %nextBrick]; + + if(%nId $= "") + { + //Don't add down bricks if we're searching up + if(%direction == 1) + continue; + + if(%queueCount >= %brickLimit) + continue; + + //Check trust + if(!ndTrustCheckSelect(%nextBrick, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + $NS[%this, "B", %queueCount] = %nextBrick; + $NS[%this, "I", %nextBrick] = %queueCount; + %queueCount++; + } + else if(%brickIsNew) + { + //If this brick already exists, we have to add the connection now + //(Start brick won't be processed again unlike the others) + $NS[%this, "C", %brickIndex, %conns] = %nId; + %conns++; + + %ci = $NS[%this, "N", %nId]++; + $NS[%this, "C", %nId, %ci - 1] = %brickIndex; + + if(%ci > %this.maxConnections) + %this.maxConnections = %ci; + + %this.connectionCount++; + } + } + + $NS[%this, "N", %brickIndex] = %conns; + + //Inc number of connections + %this.connectionCount += %conns; + + if(%conns > %this.maxConnections) + %this.maxConnections = %conns; + + %this.trustFailCount += %trustFailCount; + %this.queueCount = %queueCount; + %this.brickCount = %brickCount; + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + //First selection tick + %this.selectionStart = %queueCount; + + if(%queueCount > %brickCount) + %this.tickStackSelection(%direction, %limited, %heightLimit, %brickLimit); + else + %this.finishStackSelection(); +} + +//Tick stack selection +function ND_Selection::tickStackSelection(%this, %direction, %limited, %heightLimit, %brickLimit) +{ + cancel(%this.stackSelectSchedule); + + %highlightGroup = %this.highlightGroup; + %selectionStart = %this.selectionStart; + %queueCount = %this.queueCount; + + //Continue processing where we left off last tick + %start = %this.brickCount; + %end = %start + $Pref::Server::ND::ProcessPerTick; + + //Variables for trust checks + %admin = %this.client.isAdmin; + %group = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + + for(%i = %start; %i < %end; %i++) + { + //If no more bricks are queued, we're done! + if(%i >= %queueCount) + { + %this.queueCount = %queueCount; + %this.brickCount = %i; + + if(%i >= %brickLimit) + %this.brickLimitReached = true; + + %this.finishStackSelection(); + return; + } + + //Record data for next brick in queue + %brick = ND_Selection::recordBrickData(%this, %i); + + if(!%brick) + { + messageClient(%this.client, 'MsgError', "\c0Error: \c6Queued brick does not exist anymore. Do not modify the build during selection!"); + + %this.cancelStackSelection(); + %this.client.ndSetMode(NDM_StackSelect); + return; + } + + ndHighlightBrick(%highlightGroup, %brick); + + //Queue all up bricks + %upCount = %brick.getNumUpBricks(); + %conns = 0; + + for(%j = 0; %j < %upCount; %j++) + { + %nextBrick = %brick.getUpBrick(%j); + + //Skip bricks out of the limit + if(%limited && %direction == 0 && getWord(%nextBrick.getWorldBox(), 5) > %heightLimit) + continue; + + //If the brick is not in the selection yet, add it to the queue to get an id + %nId = $NS[%this, "I", %nextBrick]; + + if(%nId $= "") + { + if(%queueCount >= %brickLimit) + continue; + + //Check trust + if(!ndTrustCheckSelect(%nextBrick, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + $NS[%this, "B", %queueCount] = %nextBrick; + $NS[%this, "I", %nextBrick] = %queueCount; + %nId = %queueCount; + %queueCount++; + } + + $NS[%this, "C", %i, %conns] = %nId; + %conns++; + + //If this brick is from a previous stack selection, + //we need to link the connection back as well + if(%nId < %selectionStart) + { + %ci = $NS[%this, "N", %nId]++; + $NS[%this, "C", %nId, %ci - 1] = %i; + + if(%ci > %this.maxConnections) + %this.maxConnections = %ci; + + %this.connectionCount++; + } + } + + //Queue all down bricks + %downCount = %brick.getNumDownBricks(); + + for(%j = 0; %j < %downCount; %j++) + { + %nextBrick = %brick.getDownBrick(%j); + + //Skip bricks out of the limit + if(%limited && %direction == 1 && getWord(%nextBrick.getWorldBox(), 2) < %heightLimit) + continue; + + //If the brick is not in the selection yet, add it to the queue to get an id + %nId = $NS[%this, "I", %nextBrick]; + + if(%nId $= "") + { + if(%queueCount >= %brickLimit) + continue; + + //Check trust + if(!ndTrustCheckSelect(%nextBrick, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + $NS[%this, "B", %queueCount] = %nextBrick; + $NS[%this, "I", %nextBrick] = %queueCount; + %nId = %queueCount; + %queueCount++; + } + + $NS[%this, "C", %i, %conns] = %nId; + %conns++; + + //If this brick is from a previous stack selection, + //we need to link the connection back as well + if(%nId < %selectionStart) + { + %ci = $NS[%this, "N", %nId]++; + $NS[%this, "C", %nId, %ci - 1] = %i; + + if(%ci > %this.maxConnections) + %this.maxConnections = %ci; + + %this.connectionCount++; + } + } + + $NS[%this, "N", %i] = %conns; + + //Inc number of connections + %this.connectionCount += %conns; + + if(%conns > %this.maxConnections) + %this.maxConnections = %conns; + } + + %this.trustFailCount += %trustFailCount; + %this.queueCount = %queueCount; + %this.brickCount = %i; + + if(%i >= %brickLimit) + { + %this.brickLimitReached = true; + %this.finishStackSelection(); + return; + } + + //Tell the client how much we selected this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + //Schedule next tick + %this.stackSelectSchedule = %this.schedule(30, tickStackSelection, %direction, %limited, %heightLimit, %brickLimit); +} + +//Finish stack selection +function ND_Selection::finishStackSelection(%this) +{ + %this.updateSize(); + %this.updateHighlightBox(); + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadEnd', ""); + + %s = %this.brickCount == 1 ? "" : "s"; + %msg = "\c6Selected \c3" @ %this.brickCount @ "\c6 Brick" @ %s @ "!"; + + if(%this.brickLimitReached) + %msg = %msg @ " (Limit Reached)"; + + if(%this.trustFailCount > 0) + %msg = %msg @ "\n\c3" @ %this.trustFailCount @ "\c6 missing trust."; + + commandToClient(%this.client, 'centerPrint', %msg, 5); + + %this.client.ndSetMode(NDM_StackSelect); +} + +//Cancel stack selection +function ND_Selection::cancelStackSelection(%this) +{ + cancel(%this.stackSelectSchedule); + %this.deleteData(); +} + + + +//Box Selection +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Begin box selection +function ND_Selection::startBoxSelection(%this, %box, %limited) +{ + //Ensure there is no highlight group + %this.deHighlight(); + + //Save the chunk sizes + %this.chunkX1 = getWord(%box, 0); + %this.chunkY1 = getWord(%box, 1); + %this.chunkZ1 = getWord(%box, 2); + %this.chunkX2 = getWord(%box, 3); + %this.chunkY2 = getWord(%box, 4); + %this.chunkZ2 = getWord(%box, 5); + + %this.chunkSize = $Pref::Server::ND::BoxSelectChunkDim; + + %this.numChunksX = mCeil((%this.chunkX2 - %this.chunkX1) / %this.chunkSize); + %this.numChunksY = mCeil((%this.chunkY2 - %this.chunkY1) / %this.chunkSize); + %this.numChunksZ = mCeil((%this.chunkZ2 - %this.chunkZ1) / %this.chunkSize); + %this.numChunks = %this.numChunksX * %this.numChunksY * %this.numChunksZ; + + %this.currChunkX = 0; + %this.currChunkY = 0; + %this.currChunkZ = 0; + %this.currChunk = 0; + + %this.queueCount = 0; + %this.brickCount = 0; + + %this.trustFailCount = 0; + %this.brickLimitReached = false; + + %this.maxConnections = 0; + %this.connectionCount = 0; + + if(%this.client.isAdmin) + %brickLimit = $Pref::Server::ND::MaxBricksAdmin; + else + %brickLimit = $Pref::Server::ND::MaxBricksPlayer; + + //Process first tick + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + %this.tickBoxSelectionChunk(%limited, %brickLimit); +} + +//Queue all bricks in a chunk +function ND_Selection::tickBoxSelectionChunk(%this, %limited, %brickLimit) +{ + cancel(%this.boxSelectSchedule); + + //Restore chunk variables (scopes and slow object fields suck) + %chunkSize = %this.chunkSize; + %currChunk = %this.currChunk; + + %currChunkX = %this.currChunkX; + %currChunkY = %this.currChunkY; + %currChunkZ = %this.currChunkZ; + + %numChunksX = %this.numChunksX; + %numChunksY = %this.numChunksY; + %numChunksZ = %this.numChunksZ; + + %chunkX1 = %this.chunkX1; + %chunkY1 = %this.chunkY1; + %chunkZ1 = %this.chunkZ1; + + %chunkX2 = %this.chunkX2; + %chunkY2 = %this.chunkY2; + %chunkZ2 = %this.chunkZ2; + + //Where to insert bricks in the queue + %queueIndex = %this.queueCount; + + //Variables for trust checks + %admin = %this.client.isAdmin; + %group = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + + %chunksDone = 0; + %bricksFound = 0; + %trustFailCount = 0; + + //Process chunks until we reach the brick or chunk limit + while(%chunksDone < 600 && %bricksFound < 1000) + { + %chunksDone++; + + //Calculate position and size of chunk + %x1 = %chunkX1 + (%currChunkX * %chunkSize) + 0.05; + %y1 = %chunkY1 + (%currChunkY * %chunkSize) + 0.05; + %z1 = %chunkZ1 + (%currChunkZ * %chunkSize) + 0.05; + + %x2 = getMin(%chunkX2 - 0.05, %x1 + %chunkSize - 0.1); + %y2 = getMin(%chunkY2 - 0.05, %y1 + %chunkSize - 0.1); + %z2 = getMin(%chunkZ2 - 0.05, %z1 + %chunkSize - 0.1); + + %size = %x2 - %x1 SPC %y2 - %y1 SPC %z2 - %z1; + %pos = vectorAdd(%x1 SPC %y1 SPC %z1, vectorScale(%size, 0.5)); + + //Queue all new bricks found in this chunk + initContainerBoxSearch(%pos, %size, $TypeMasks::FxBrickAlwaysObjectType); + + while(%obj = containerSearchNext()) + { + %bricksFound++; + + if($NS[%this, "I", %obj] $= "") + { + if(%limited) + { + //Skip bricks that are outside the limit + %box = %obj.getWorldBox(); + + if(getWord(%box, 0) < %chunkX1 - 0.1) + continue; + + if(getWord(%box, 1) < %chunkY1 - 0.1) + continue; + + if(getWord(%box, 2) < %chunkZ1 - 0.1) + continue; + + if(getWord(%box, 3) > %chunkX2 + 0.1) + continue; + + if(getWord(%box, 4) > %chunkY2 + 0.1) + continue; + + if(getWord(%box, 5) > %chunkZ2 + 0.1) + continue; + } + + //Check trust + if(!ndTrustCheckSelect(%obj, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + //Queue brick + $NS[%this, "I", %obj] = %queueIndex; + $NS[%this, "B", %queueIndex] = %obj; + %queueIndex++; + + //Test brick limit + if(%queueIndex >= %brickLimit) + { + %limitReached = true; + break; + } + } + } + + //Stop processing chunks if limit was reached + if(%limitReached) + break; + + //Set next chunk index or break + %currChunk++; + + if(%currChunkX++ >= %numChunksX) + { + %currChunkX = 0; + + if(%currChunkY++ >= %numChunksY) + { + %currChunkY = 0; + + if(%currChunkZ++ >= %numChunksZ) + { + %searchComplete = true; + break; + } + } + } + } + + //Save chunk variables (scopes and slow object fields suck) + %this.currChunk = %currChunk; + + %this.currChunkX = %currChunkX; + %this.currChunkY = %currChunkY; + %this.currChunkZ = %currChunkZ; + + %this.numChunksX = %numChunksX; + %this.numChunksY = %numChunksY; + %this.numChunksZ = %numChunksZ; + + %this.trustFailCount += %trustFailCount; + %this.queueCount = %queueIndex; + + //If the brick limit was reached, start processing + if(%limitReached) + { + %this.brickLimitReached = true; + %this.rootPosition = $NS[%this, "B", 0].getPosition(); + %this.boxSelectSchedule = %this.schedule(30, tickBoxSelectionProcess); + + return; + } + + //If all chunks have been searched, start processing + if(%searchComplete) + { + //Did we find any bricks at all? + if(%queueIndex > 0) + { + //Create highlight group + %this.highlightGroup = ndNewHighlightGroup(); + + //Start processing bricks + %this.rootPosition = $NS[%this, "B", 0].getPosition(); + %this.boxSelectSchedule = %this.schedule(30, tickBoxSelectionProcess); + } + else + { + messageClient(%this.client, 'MsgError', ""); + + %m = "\c6No bricks were found inside the selection!"; + + if(%this.trustFailCount > 0) + %m = %m @ "\n\c3" @ %this.trustFailCount @ "\c6 missing trust."; + + commandToClient(%this.client, 'centerPrint', %m, 5); + + %this.cancelBoxSelection(); + %this.client.ndSetMode(NDM_BoxSelect); + } + + return; + } + + //Tell the client which chunks we just processed + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + //Schedule next chunk + %this.boxSelectSchedule = %this.schedule(30, tickBoxSelectionChunk, %limited, %brickLimit); +} + +//Save connections between bricks and highlight them +function ND_Selection::tickBoxSelectionProcess(%this) +{ + cancel(%this.boxSelectSchedule); + %highlightGroup = %this.highlightGroup; + + //Get bounds for this tick + %start = %this.brickCount; + %end = %start + $Pref::Server::ND::ProcessPerTick; + + if(%end > %this.queueCount) + %end = %this.queueCount; + + //Save connections for bricks in the list + for(%i = %start; %i < %end; %i++) + { + //Record data for next brick in queue + %brick = ND_Selection::recordBrickData(%this, %i); + + if(!%brick) + { + messageClient(%this.client, 'MsgError', "\c0Error: \c6Queued brick does not exist anymore. Do not modify the build during selection!"); + + %this.cancelBoxSelection(); + %this.client.ndSetMode(NDM_BoxSelect); + return; + } + + ndHighlightBrick(%highlightGroup, %brick); + + //Save all up bricks + %upCount = %brick.getNumUpBricks(); + %conns = 0; + + for(%j = 0; %j < %upCount; %j++) + { + %conn = %brick.getUpBrick(%j); + + //If the brick is in the selection, save the connection + if((%nId = $NS[%this, "I", %conn]) !$= "") + { + $NS[%this, "C", %i, %conns] = %nId; + %conns++; + } + } + + //Save all down bricks + %downCount = %brick.getNumDownBricks(); + + for(%j = 0; %j < %downCount; %j++) + { + %conn = %brick.getDownBrick(%j); + + //If the brick is in the selection, save the connection + if((%nId = $NS[%this, "I", %conn]) !$= "") + { + $NS[%this, "C", %i, %conns] = %nId; + %conns++; + } + } + + $NS[%this, "N", %i] = %conns; + + //Inc number of connections + %this.connectionCount += %conns; + + if(%conns > %this.maxConnections) + %this.maxConnections = %conns; + } + + //Save how far we got + %this.brickCount = %i; + + //Tell the client how much we selected this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + if(%i >= %this.queueCount) + %this.finishBoxSelection(); + else + %this.boxSelectSchedule = %this.schedule(30, tickBoxSelectionProcess); +} + +//Finish box selection +function ND_Selection::finishBoxSelection(%this) +{ + %this.updateSize(); + %this.updateHighlightBox(); + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadEnd', ""); + + %s = %this.brickCount == 1 ? "" : "s"; + %msg = "\c6Selected \c3" @ %this.brickCount @ "\c6 Brick" @ %s @ "!"; + + if(%this.brickLimitReached) + %msg = %msg @ " (Limit Reached)"; + + if(%this.trustFailCount > 0) + %msg = %msg @ "\n\c3" @ %this.trustFailCount @ "\c6 missing trust."; + + %msg = %msg @ "\n\c6Press [Cancel Brick] to adjust the box."; + commandToClient(%this.client, 'centerPrint', %msg, 8); + + %this.client.ndSetMode(NDM_BoxSelect); +} + +//Cancel box selection +function ND_Selection::cancelBoxSelection(%this) +{ + cancel(%this.boxSelectSchedule); + %this.deleteData(); +} + + + +//Recording Brick Data +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Record info about a queued brick +function ND_Selection::recordBrickData(%this, %i) +{ + //Return false if brick no longer exists + if(!isObject(%brick = $NS[%this, "B", %i])) + return false; + + /////////////////////////////////////////////////////////// + //Variables required for every brick + + //Datablock + %datablock = %brick.getDatablock(); + $NS[%this, "D", %i] = %datablock; + + //Offset from base brick + $NS[%this, "P", %i] = vectorSub(%brick.getPosition(), %this.rootPosition); + + //Rotation + $NS[%this, "R", %i] = %brick.angleID; + + //Colors + if($NDHN[%brick]) + { + $NS[%this, "CO", %i] = %brick.colorID; + $NS[%this, "CF", %i] = $NDHF[%brick]; + } + else + { + $NS[%this, "CO", %i] = %brick.colorID; + + if(%brick.colorFxID) + $NS[%this, "CF", %i] = %brick.colorFxID; + } + + /////////////////////////////////////////////////////////// + //Optional variables only required for few bricks + + if(%tmp = %brick.shapeFxID) + $NS[%this, "SF", %i] = %tmp; + + //Wrench settings + if((%tmp = %brick.getName()) !$= "") + { + $NS[%this, "HN", %tmp] = true; + $NS[%this, "NT", %i] = getSubStr(%tmp, 1, 254); + } + + if(%tmp = %brick.light | 0) + $NS[%this, "LD", %i] = %tmp.getDatablock(); + + if(%tmp = %brick.emitter | 0) + { + $NS[%this, "ED", %i] = %tmp.getEmitterDatablock(); + $NS[%this, "ER", %i] = %brick.emitterDirection; + } + + if(%tmp = %brick.item | 0) + { + $NS[%this, "ID", %i] = %tmp.getDatablock(); + $NS[%this, "IP", %i] = %brick.itemPosition; + $NS[%this, "IR", %i] = %brick.itemDirection; + $NS[%this, "IT", %i] = %brick.itemRespawnTime; + } + + if(%tmp = %brick.vehicleDataBlock) + { + $NS[%this, "VD", %i] = %tmp; + $NS[%this, "VC", %i] = %brick.reColorVehicle; + } + + if(%tmp = %brick.AudioEmitter | 0) + $NS[%this, "MD", %i] = %tmp.profile.getID(); + + if(!%brick.isRaycasting()) + $NS[%this, "NRC", %i] = true; + + if(!%brick.isColliding()) + $NS[%this, "NC", %i] = true; + + if(!%brick.isRendering()) + $NS[%this, "NR", %i] = true; + + //Prints + if(%datablock.hasPrint) + $NS[%this, "PR", %i] = %brick.printID; + + //Events + if(%numEvents = %brick.numEvents) + { + $NS[%this, "EN", %i] = %numEvents; + + for(%j = 0; %j < %numEvents; %j++) + { + $NS[%this, "EE", %i, %j] = %brick.eventEnabled[%j]; + $NS[%this, "ED", %i, %j] = %brick.eventDelay[%j]; + + $NS[%this, "EI", %i, %j] = %brick.eventInput[%j]; + $NS[%this, "EII", %i, %j] = %brick.eventInputIdx[%j]; + + $NS[%this, "EO", %i, %j] = %brick.eventOutput[%j]; + $NS[%this, "EOI", %i, %j] = %brick.eventOutputIdx[%j]; + $NS[%this, "EOC", %i, %j] = %brick.eventOutputAppendClient[%j]; + + %target = %brick.eventTargetIdx[%j]; + + if(%target == -1) + $NS[%this, "ENT", %i, %j] = %brick.eventNT[%j]; + + $NS[%this, "ET", %i, %j] = %brick.eventTarget[%j]; + $NS[%this, "ETI", %i, %j] = %target; + + $NS[%this, "EP", %i, %j, 0] = %brick.eventOutputParameter[%j, 1]; + $NS[%this, "EP", %i, %j, 1] = %brick.eventOutputParameter[%j, 2]; + $NS[%this, "EP", %i, %j, 2] = %brick.eventOutputParameter[%j, 3]; + $NS[%this, "EP", %i, %j, 3] = %brick.eventOutputParameter[%j, 4]; + } + } + + //Update total selection size + %box = %brick.getWorldBox(); + %minX = getWord(%box, 0); + %minY = getWord(%box, 1); + %minZ = getWord(%box, 2); + %maxX = getWord(%box, 3); + %maxY = getWord(%box, 4); + %maxZ = getWord(%box, 5); + + if(%i) + { + if(%minX < %this.minX) + %this.minX = %minX; + + if(%minY < %this.minY) + %this.minY = %minY; + + if(%minZ < %this.minZ) + %this.minZ = %minZ; + + if(%maxX > %this.maxX) + %this.maxX = %maxX; + + if(%maxY > %this.maxY) + %this.maxY = %maxY; + + if(%maxZ > %this.maxZ) + %this.maxZ = %maxZ; + } + else + { + %this.minX = %minX; + %this.minY = %minY; + %this.minZ = %minZ; + %this.maxX = %maxX; + %this.maxY = %maxY; + %this.maxZ = %maxZ; + } + + return %brick; +} + + + +//Highlighting bricks +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Set the size variables after selecting bricks +function ND_Selection::updateSize(%this) +{ + %this.minSize = vectorSub(%this.minX SPC %this.minY SPC %this.minZ, %this.rootPosition); + %this.maxSize = vectorSub(%this.maxX SPC %this.maxY SPC %this.maxZ, %this.rootPosition); + + %this.brickSizeX = mFloatLength((%this.maxX - %this.minX) * 2, 0); + %this.brickSizeY = mFloatLength((%this.maxY - %this.minY) * 2, 0); + %this.brickSizeZ = mFloatLength((%this.maxZ - %this.minZ) * 5, 0); + + %this.rootToCenter = vectorAdd(%this.minSize, vectorScale(vectorSub(%this.maxSize, %this.minSize), 0.5)); +} + +//Create or update the highlight box +function ND_Selection::updateHighlightBox(%this) +{ + if(!isObject(%this.highlightBox)) + %this.highlightBox = ND_HighlightBox(); + + if(!isObject(%this.ghostGroup)) + { + %min = vectorAdd(%this.rootPosition, %this.minSize); + %max = vectorAdd(%this.rootPosition, %this.maxSize); + %this.highlightBox.setSize(%min, %max); + } + else + %this.highlightBox.setSize(%this.getGhostWorldBox()); +} + +//Remove the highlight box +function ND_Selection::deleteHighlightBox(%this) +{ + if(isObject(%this.highlightBox)) + %this.highlightBox.delete(); +} + +//Start clearing the highlight set +function ND_Selection::deHighlight(%this) +{ + if(%this.highlightGroup) + { + ndStartDeHighlight(%this.highlightGroup); + %this.highlightGroup = 0; + } +} + + + +//Cutting bricks +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +//Begin cutting +function ND_Selection::startCutting(%this) +{ + //Process first tick + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + %this.cutIndex = 0; + %this.cutSuccessCount = 0; + %this.cutFailCount = 0; + + %this.tickCutting(); +} + +//Cut some bricks +function ND_Selection::tickCutting(%this) +{ + cancel(%this.cutSchedule); + + //Get bounds for this tick + %start = %this.cutIndex; + %end = %start + $Pref::Server::ND::ProcessPerTick; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + %cutSuccessCount = %this.cutSuccessCount; + %cutFailCount = %this.cutFailCount; + + %admin = %this.client.isAdmin; + %group = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + + //Cut bricks + for(%i = %start; %i < %end; %i++) + { + %brick = $NS[%this, "B", %i]; + + if(!isObject(%brick)) + continue; + + if(!ndTrustCheckModify(%brick, %group, %bl_id, %admin)) + { + %cutFailCount++; + continue; + } + + // Support for hole bots + if(isObject(%brick.hBot)) + { + %brick.hBot.spawnProjectile("audio2d", "deathProjectile", "0 0 0", 1); + %brick.hBot.delete(); + } + + %brick.delete(); + %cutSuccessCount++; + } + + //Save how far we got + %this.cutIndex = %i; + + %this.cutSuccessCount = %cutSuccessCount; + %this.cutFailCount = %cutFailCount; + + //Tell the client how much we cut this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + if(%i >= %this.brickCount) + %this.finishCutting(); + else + %this.cutSchedule = %this.schedule(30, tickCutting); +} + +//Finish cutting +function ND_Selection::finishCutting(%this) +{ + %s = %this.cutSuccessCount == 1 ? "" : "s"; + %msg = "\c6Cut \c3" @ %this.cutSuccessCount @ "\c6 Brick" @ %s @ "!"; + + if(%this.cutFailCount > 0) + %msg = %msg @ "\n\c3" @ %this.cutFailCount @ "\c6 missing trust."; + + commandToClient(%this.client, 'centerPrint', %msg, 8); + + %this.client.ndSetMode(NDM_PlantCopy); +} + +//Cancel cutting +function ND_Selection::cancelCutting(%this) +{ + cancel(%this.cutSchedule); +} + + + +//Supercut +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Begin supercut +function ND_Selection::startSuperCut(%this, %box) +{ + //Ensure there is no highlight group + %this.deHighlight(); + + //Save the chunk sizes + %this.chunkX1 = getWord(%box, 0); + %this.chunkY1 = getWord(%box, 1); + %this.chunkZ1 = getWord(%box, 2); + %this.chunkX2 = getWord(%box, 3); + %this.chunkY2 = getWord(%box, 4); + %this.chunkZ2 = getWord(%box, 5); + + %this.chunkSize = $Pref::Server::ND::BoxSelectChunkDim; + + %this.numChunksX = mCeil((%this.chunkX2 - %this.chunkX1) / %this.chunkSize); + %this.numChunksY = mCeil((%this.chunkY2 - %this.chunkY1) / %this.chunkSize); + %this.numChunksZ = mCeil((%this.chunkZ2 - %this.chunkZ1) / %this.chunkSize); + %this.numChunks = %this.numChunksX * %this.numChunksY * %this.numChunksZ; + + %this.currChunkX = 0; + %this.currChunkY = 0; + %this.currChunkZ = 0; + %this.currChunk = 0; + + %this.trustFailCount = 0; + %this.superCutCount = 0; + %this.superCutPlacedCount = 0; + + //Process first tick + if(%client && $Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + %this.tickSuperCutChunk(); +} + +//Process all bricks in a chunk +function ND_Selection::tickSuperCutChunk(%this) +{ + cancel(%this.superCutSchedule); + + //Restore chunk variables (scopes and slow object fields suck) + %chunkSize = %this.chunkSize; + %currChunk = %this.currChunk; + + %currChunkX = %this.currChunkX; + %currChunkY = %this.currChunkY; + %currChunkZ = %this.currChunkZ; + + %numChunksX = %this.numChunksX; + %numChunksY = %this.numChunksY; + %numChunksZ = %this.numChunksZ; + + %chunkX1 = %this.chunkX1; + %chunkY1 = %this.chunkY1; + %chunkZ1 = %this.chunkZ1; + + %chunkX2 = %this.chunkX2; + %chunkY2 = %this.chunkY2; + %chunkZ2 = %this.chunkZ2; + + //Variables for trust checks + if(%this.client) + { + %admin = %this.client.isAdmin; + %group = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + } + + %chunksDone = 0; + %bricksFound = 0; + %bricksPlanted = 0; + %trustFailCount = 0; + + ndUpdateSpawnedClientList(); + + //Process chunks until we reach the brick or chunk limit + while(%chunksDone < 600 && %bricksFound < 1000 && %bricksPlanted < 300) + { + %chunksDone++; + + //Calculate position and size of chunk + %x1 = %chunkX1 + (%currChunkX * %chunkSize) + 0.05; + %y1 = %chunkY1 + (%currChunkY * %chunkSize) + 0.05; + %z1 = %chunkZ1 + (%currChunkZ * %chunkSize) + 0.05; + + %x2 = getMin(%chunkX2 - 0.05, %x1 + %chunkSize - 0.1); + %y2 = getMin(%chunkY2 - 0.05, %y1 + %chunkSize - 0.1); + %z2 = getMin(%chunkZ2 - 0.05, %z1 + %chunkSize - 0.1); + + %size = %x2 - %x1 SPC %y2 - %y1 SPC %z2 - %z1; + %pos = vectorAdd(%x1 SPC %y1 SPC %z1, vectorScale(%size, 0.5)); + + //Process all new bricks found in this chunk + initContainerBoxSearch(%pos, %size, $TypeMasks::FxBrickAlwaysObjectType); + + while(%obj = containerSearchNext()) + { + %db = %obj.getDatablock(); + %bricksFound++; + + //Check trust + if(%this.client && !ndTrustCheckModify(%obj, %group, %bl_id, %admin)) + { + %trustFailCount++; + continue; + } + + //Skip zone bricks + if(%db.isWaterBrick) + continue; + + //Skip dead bricks + if(%obj.isDead()) + continue; + + //Set variables for the fill brick function + $ND::FillBrickGroup = %obj.getGroup(); + $ND::FillBrickClient = %obj.client; + $ND::FillBrickBL_ID = %obj.getGroup().bl_id; + + $ND::FillBrickColorID = %obj.colorID; + $ND::FillBrickColorFxID = %obj.colorFxID; + $ND::FillBrickShapeFxID = %obj.shapeFxID; + + $ND::FillBrickRendering = %obj.isRendering(); + $ND::FillBrickColliding = %obj.isColliding(); + $ND::FillBrickRayCasting = %obj.isRayCasting(); + + $ND::FillBrickSubset = %obj.getDatablock().ndSubset; + + %box = %obj.getWorldBox(); + %boxX1 = getWord(%box, 0); + %boxY1 = getWord(%box, 1); + %boxZ1 = getWord(%box, 2); + %boxX2 = getWord(%box, 3); + %boxY2 = getWord(%box, 4); + %boxZ2 = getWord(%box, 5); + %obj.delete(); + + %deleted = true; + %cutCount++; + + $ND::FillBrickCount = 0; + + if((%boxX1 + 0.05) < %chunkX1) + { + ndFillAreaWithBricks( + %boxX1 SPC %boxY1 SPC %boxZ1, + %chunkX1 SPC %boxY2 SPC %boxZ2); + } + + if((%boxX2 - 0.05) > %chunkX2) + { + ndFillAreaWithBricks( + %chunkX2 SPC %boxY1 SPC %boxZ1, + %boxX2 SPC %boxY2 SPC %boxZ2); + } + + if((%boxY1 + 0.05) < %chunkY1) + { + ndFillAreaWithBricks( + getMax(%boxX1, %chunkX1) SPC %boxY1 SPC %boxZ1, + getMin(%boxX2, %chunkX2) SPC %chunkY1 SPC %boxZ2); + } + + if((%boxY2 - 0.05) > %chunkY2) + { + ndFillAreaWithBricks( + getMax(%boxX1, %chunkX1) SPC %chunkY2 SPC %boxZ1, + getMin(%boxX2, %chunkX2) SPC %boxY2 SPC %boxZ2); + } + + if((%boxZ1 + 0.05) < %chunkZ1) + { + ndFillAreaWithBricks( + getMax(%boxX1, %chunkX1) SPC getMax(%boxY1, %chunkY1) SPC %boxZ1, + getMin(%boxX2, %chunkX2) SPC getMin(%boxY2, %chunkY2) SPC %chunkZ1); + } + + if((%boxZ2 - 0.05) > %chunkZ2) + { + ndFillAreaWithBricks( + getMax(%boxX1, %chunkX1) SPC getMax(%boxY1, %chunkY1) SPC %chunkZ2, + getMin(%boxX2, %chunkX2) SPC getMin(%boxY2, %chunkY2) SPC %boxZ2); + } + + %bricksPlanted += $ND::FillBrickCount; + } + + //Set next chunk index or break + %currChunk++; + + if(%currChunkX++ >= %numChunksX) + { + %currChunkX = 0; + + if(%currChunkY++ >= %numChunksY) + { + %currChunkY = 0; + + if(%currChunkZ++ >= %numChunksZ) + { + %searchComplete = true; + break; + } + } + } + } + + //Save chunk variables (scopes and slow object fields suck) + %this.currChunk = %currChunk; + + %this.currChunkX = %currChunkX; + %this.currChunkY = %currChunkY; + %this.currChunkZ = %currChunkZ; + + %this.numChunksX = %numChunksX; + %this.numChunksY = %numChunksY; + %this.numChunksZ = %numChunksZ; + + %this.trustFailCount += %trustFailCount; + %this.superCutCount += %cutCount; + %this.superCutPlacedCount += %bricksPlanted; + + //If all chunks have been searched, start processing + if(%searchComplete) + { + %this.finishSuperCut(); + return; + } + + //Tell the client which chunks we just processed + if(%this.client && %this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + //Schedule next chunk + %this.superCutSchedule = %this.schedule(30, tickSuperCutChunk); +} + +//Finish super cut +function ND_Selection::finishSuperCut(%this) +{ + if(!%this.client) + return; + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadEnd', ""); + + %s = %this.superCutCount == 1 ? "" : "s"; + %msg = "\c6Deleted \c3" @ %this.superCutCount @ "\c6 Brick" @ %s @ "!"; + + if(%this.superCutPlacedCount > 0) + { + %s = %this.superCutPlacedCount == 1 ? "" : "s"; + %msg = %msg @ "\n\c6Placed \c3" @ %this.superCutPlacedCount @ "\c6 new one" @ %s @ "."; + } + + if(%this.trustFailCount > 0) + %msg = %msg @ "\n\c3" @ %this.trustFailCount @ "\c6 missing trust."; + + commandToClient(%this.client, 'centerPrint', %msg, 12); + %this.client.ndSetMode(NDM_BoxSelect); + + if(%this.client.fillBricksAfterSuperCut) + { + %this.client.fillBricksAfterSuperCut = false; + + if(%this.trustFailCount) + messageClient(%this.client, '', "\c6Cannot run fill bricks, you do not have enough trust bricks already in the area."); + else + %this.client.doFillBricks(%this.client.NDFillBrickSubset); + } +} + +//Cancel super cut +function ND_Selection::cancelSuperCut(%this) +{ + cancel(%this.superCutSchedule); + %this.deleteData(); +} + + + +//Ghost bricks +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Spawn ghost bricks at a specific location +function ND_Selection::spawnGhostBricks(%this, %position, %angleID) +{ + %this.ghostMirrorX = false; + %this.ghostMirrorY = false; + %this.ghostMirrorZ = false; + + //Create group to hold the ghost bricks + %this.ghostGroup = ND_GhostGroup(); + + //Scoping is broken for ghost bricks, make temp list of spawned clients to use later + ndUpdateSpawnedClientList(); + + //Figure out correct increment to spawn no more than the max number of ghost bricks + %max = %this.brickCount; + %increment = 1; + + if(%max > $Pref::Server::ND::MaxGhostBricks) + { + if($Pref::Server::ND::ScatterGhostBricks) + %increment = %max / $Pref::Server::ND::MaxGhostBricks; + else + %max = $Pref::Server::ND::MaxGhostBricks; + } + + %ghostGroup = %this.ghostGroup; + + //Spawn ghost bricks + for(%f = 0; %f < %max; %f += %increment) + { + %i = mFloor(%f); + + //Skip missing bricks + if($NS[%this, "D", %i] == 0) + continue; + + //Offset position + %bPos = vectorAdd(ndRotateVector($NS[%this, "P", %i], %angleID), %position); + + //Rotate local angle id and get correct rotation value + %bAngle = ($NS[%this, "R", %i] + %angleID ) % 4; + + switch(%bAngle) + { + case 0: %bRot = "1 0 0 0"; + case 1: %bRot = "0 0 1 90.0002"; + case 2: %bRot = "0 0 1 180"; + case 3: %bRot = "0 0 -1 90.0002"; + } + + //Spawn ghost brick + %brick = new FxDTSBrick() + { + datablock = $NS[%this, "D", %i]; + isPlanted = false; + + position = %bPos; + rotation = %bRot; + angleID = %bAngle; + + colorID = $NS[%this, "CO", %i]; + printID = $NS[%this, "PR", %i]; + + //Used in shiftGhostBricks + selectionIndex = %i; + }; + + //Add ghost brick to ghost set + %ghostGroup.add(%brick); + + //Scope ghost brick to all clients we found earlier + for(%j = 0; %j < $ND::NumSpawnedClients; %j++) + %brick.scopeToClient($ND::SpawnedClient[%j]); + } + + //Update variables + %this.ghostPosition = %position; + %this.ghostAngleID = %angleID; + + //Change highlightbox to blue + %this.highlightBox.color = "0.2 0.2 1 0.99"; + %this.highlightBox.applyColors(); + %this.updateHighlightBox(); +} + +//Move ghost bricks to an offset position +function ND_Selection::shiftGhostBricks(%this, %offset) +{ + //Fit to grid + %x = mFloatLength(getWord(%offset, 0) * 2, 0) / 2; + %y = mFloatLength(getWord(%offset, 1) * 2, 0) / 2; + %z = mFloatLength(getWord(%offset, 2) * 5, 0) / 5; + + if(%x == 0 && %y == 0 && %z == 0) + return; + + //Update variables + %this.ghostPosition = vectorAdd(%this.ghostPosition, %x SPC %y SPC %z); + %this.updateHighlightBox(); + + //Update ghost bricks + %this.updateGhostBricks(0, $Pref::Server::ND::InstantGhostBricks, 230); +} + +//Rotate ghost bricks left/right +function ND_Selection::rotateGhostBricks(%this, %direction, %useSelectionCenter) +{ + //First brick is root brick + %rootBrick = %this.ghostGroup.getObject(0); + + //Figure out the pivot and shift values + if(%useSelectionCenter) + { + %pivot = %this.getGhostCenter(); + + %brickSizeX = %this.brickSizeX; + %brickSizeY = %this.brickSizeY; + } + else + { + %pivot = %this.ghostPosition; + + %brickSizeX = %rootBrick.getDatablock().brickSizeX; + %brickSizeY = %rootBrick.getDatablock().brickSizeY; + } + + //Even x odd sized rectangles can't be rotated around their center to stay in the grid + %shiftCorrect = "0 0 0"; + + if((%brickSizeX % 2) != (%brickSizeY % 2)) + { + if(%this.ghostAngleID % 2) + %shiftCorrect = "-0.25 -0.25 0"; + else + %shiftCorrect = "0.25 0.25 0"; + } + + //Get vector from pivot to root brick + %pOffset = vectorSub(%rootBrick.getPosition(), %pivot); + + //Rotate offset vector 90 degrees + %pOffset = ndRotateVector(%pOffset, %direction); + + //Add shift correction + if(%direction % 2 != 0) + %pOffset = vectorAdd(%pOffset, %shiftCorrect); + + //Update variables + %this.ghostAngleID = (%this.ghostAngleID + %direction) % 4; + %this.ghostPosition = vectorAdd(%pivot, %pOffset); + %this.updateHighlightBox(); + + //Update ghost bricks + %this.updateGhostBricks(0, $Pref::Server::ND::InstantGhostBricks, 230); +} + +//Mirror ghost bricks on x,y,z axis +function ND_Selection::mirrorGhostBricks(%this, %axis) +{ + //Update variables + if(%axis == 0) + { + %this.ghostMirrorX = !%this.ghostMirrorX; + + //Offset ghost so we end up in the same area + if(%this.ghostMirrorX) + %offset = (getWord(%this.rootToCenter, 0) * 2) @ " 0 0"; + else + %offset = (getWord(%this.rootToCenter, 0) * -2) @ " 0 0"; + } + else if(%axis == 1) + { + %this.ghostMirrorY = !%this.ghostMirrorY; + + //Offset ghost so we end up in the same area + if(%this.ghostMirrorY) + %offset = "0 " @ (getWord(%this.rootToCenter, 1) * 2) @ " 0"; + else + %offset = "0 " @ (getWord(%this.rootToCenter, 1) * -2) @ " 0"; + } + else + { + %this.ghostMirrorZ = !%this.ghostMirrorZ; + + //Offset ghost so we end up in the same area + if(%this.ghostMirrorZ) + %offset = "0 0 " @ getWord(%this.rootToCenter, 2) * 2; + else + %offset = "0 0 " @ getWord(%this.rootToCenter, 2) * -2; + } + + //Double mirror is just a rotation + if(%this.ghostMirrorX && %this.ghostMirrorY) + { + %this.ghostAngleID = (%this.ghostAngleID + 2) % 4; + %this.ghostMirrorX = false; + %this.ghostMirrorY = false; + + if(%axis == 0) + %offset = (getWord(%this.rootToCenter, 0) * -2) @ " 0 0"; + else + %offset = "0 " @ (getWord(%this.rootToCenter, 1) * -2) @ " 0"; + } + + //If pivot is whole selection, shift bricks to keep area + if(%this.client.ndPivot) + %this.ghostPosition = vectorAdd(%this.ghostPosition, ndRotateVector(%offset, %this.ghostAngleID)); + + %this.updateHighlightBox(); + + //Update ghost bricks + %this.updateGhostBricks(0, $Pref::Server::ND::InstantGhostBricks, 230); +} + +//Update some of the ghost bricks to the latest position/rotation +function ND_Selection::updateGhostBricks(%this, %start, %count, %wait) +{ + cancel(%this.ghostMoveSchedule); + %max = %this.ghostGroup.getCount(); + + if(%max - %start > %count) + { + %max = %start + %count; + + //Start schedule to move remaining ghost bricks + %this.ghostMoveSchedule = %this.schedule(%wait, updateGhostBricks, + %max, $Pref::Server::ND::ProcessPerTick, 30); + } + + %pos = %this.ghostPosition; + %angle = %this.ghostAngleID; + %ghostGroup = %this.ghostGroup; + %mirrX = %this.ghostMirrorX; + %mirrY = %this.ghostMirrorY; + %mirrZ = %this.ghostMirrorZ; + + //Update the ghost bricks in this tick + for(%i = %start; %i < %max; %i++) + { + %brick = %ghostGroup.getObject(%i); + %j = %brick.selectionIndex; + + //Offset position + %bPos = $NS[%this, "P", %j]; + + //Rotated local angle id + %bAngle = $NS[%this, "R", %j]; + + //Apply mirror effects (ugh) + %datablock = $NS[%this, "D", %j]; + + if(%mirrX) + { + //Mirror offset + %bPos = -firstWord(%bPos) SPC restWords(%bPos); + + //Handle symmetries + switch($ND::Symmetry[%datablock]) + { + //Asymmetric + case 0: + if(%db = $ND::SymmetryXDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryXOffset[%datablock]) % 4; + + //Pair is made on X, so apply mirror logic for X afterwards + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + } + + //Do nothing for fully symmetric + + //X symmetric - rotate 180 degrees if brick is angled 90 or 270 degrees + case 2: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + + //Y symmetric - rotate 180 degrees if brick is angled 0 or 180 degrees + case 3: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + + //X+Y symmetric - rotate 90 degrees + case 4: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 1) % 4; + else + %bAngle = (%bAngle + 3) % 4; + + //X-Y symmetric - rotate -90 degrees + case 5: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 3) % 4; + else + %bAngle = (%bAngle + 1) % 4; + } + } + else if(%mirrY) + { + //Mirror offset + %bPos = getWord(%bPos, 0) SPC -getWord(%bPos, 1) SPC getWord(%bPos, 2); + + //Handle symmetries + switch($ND::Symmetry[%datablock]) + { + //Asymmetric + case 0: + if(%db = $ND::SymmetryXDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryXOffset[%datablock]) % 4; + + //Pair is made on X, so apply mirror logic for X afterwards + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + } + + //Do nothing for fully symmetric + + //X symmetric - rotate 180 degrees if brick is angled 90 or 270 degrees + case 2: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + + //Y symmetric - rotate 180 degrees if brick is angled 0 or 180 degrees + case 3: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + + //X+Y symmetric - rotate 90 degrees + case 4: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 1) % 4; + else + %bAngle = (%bAngle + 3) % 4; + + //X-Y symmetric - rotate -90 degrees + case 5: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 3) % 4; + else + %bAngle = (%bAngle + 1) % 4; + } + } + + if(%mirrZ) + { + //Mirror offset + %bPos = getWords(%bPos, 0, 1) SPC -getWord(%bPos, 2); + + //Change datablock if asymmetric + if(!$ND::SymmetryZ[%datablock]) + { + if(%db = $ND::SymmetryZDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryZOffset[%datablock]) % 4; + } + } + } + + //Apply datablock + if(%brick.getDatablock() != %datablock) + %brick.setDatablock(%datablock); + + //Rotate and add offset + %bAngle = (%bAngle + %angle) % 4; + %bPos = vectorAdd(%pos, ndRotateVector(%bPos, %angle)); + + switch(%bAngle) + { + case 0: %bRot = "1 0 0 0"; + case 1: %bRot = "0 0 1 1.5708"; + case 2: %bRot = "0 0 1 3.14150"; + case 3: %bRot = "0 0 -1 1.5708"; + } + + //Apply transform + %brick.setTransform(%bPos SPC %bRot); + } +} + +//Delete ghost bricks +function ND_Selection::deleteGhostBricks(%this) +{ + if(!isObject(%this.ghostGroup)) + return; + + cancel(%this.ghostMoveSchedule); + + %this.ghostGroup.tickDelete(); + %this.ghostGroup = false; +} + +//World box center for ghosted selection +function ND_Selection::getGhostCenter(%this) +{ + if(!isObject(%this.ghostGroup)) + return "0 0 0"; + + %offset = %this.rootToCenter; + + if(%this.ghostMirrorX) + %offset = -getWord(%offset, 0) SPC getWord(%offset, 1) SPC getWord(%offset, 2); + else if(%this.ghostMirrorY) + %offset = getWord(%offset, 0) SPC -getWord(%offset, 1) SPC getWord(%offset, 2); + + if(%this.ghostMirrorZ) + %offset = getWord(%offset, 0) SPC getWord(%offset, 1) SPC -getWord(%offset, 2); + + return vectorAdd(%this.ghostPosition, ndRotateVector(%offset, %this.ghostAngleID)); +} + +//World box for ghosted selection +function ND_Selection::getGhostWorldBox(%this) +{ + if(!isObject(%this.ghostGroup)) + return "0 0 0 0 0 0"; + + %min = %this.minSize; + %max = %this.maxSize; + + //Handle mirrors + if(%this.ghostMirrorX) + { + %min = -firstWord(%min) SPC restWords(%min); + %max = -firstWord(%max) SPC restWords(%max); + } + else if(%this.ghostMirrorY) + { + %min = getWord(%min, 0) SPC -getWord(%min, 1) SPC getWord(%min, 2); + %max = getWord(%max, 0) SPC -getWord(%max, 1) SPC getWord(%max, 2); + } + + if(%this.ghostMirrorZ) + { + %min = getWords(%min, 0, 1) SPC -getWord(%min, 2); + %max = getWords(%max, 0, 1) SPC -getWord(%max, 2); + } + + //Handle rotation + %min = ndRotateVector(%min, %this.ghostAngleID); + %max = ndRotateVector(%max, %this.ghostAngleID); + + //Get max values + %minX = getMin(getWord(%min, 0), getWord(%max, 0)); + %minY = getMin(getWord(%min, 1), getWord(%max, 1)); + %minZ = getMin(getWord(%min, 2), getWord(%max, 2)); + + %maxX = getMax(getWord(%min, 0), getWord(%max, 0)); + %maxY = getMax(getWord(%min, 1), getWord(%max, 1)); + %maxZ = getMax(getWord(%min, 2), getWord(%max, 2)); + + %pos = %this.ghostPosition; + return vectorAdd(%pos, %minX SPC %minY SPC %minZ) SPC vectorAdd(%pos, %maxX SPC %maxY SPC %maxZ); +} + + + +//Planting bricks +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Start planting bricks! +function ND_Selection::startPlant(%this, %position, %angleID, %forcePlant) +{ + %this.forcePlant = %forcePlant; + + %this.plantSearchIndex = 0; + %this.plantQueueIndex = 0; + %this.plantQueueCount = 0; + + %this.plantSuccessCount = 0; + %this.plantTrustFailCount = 0; + %this.plantBlockedFailCount = 0; + %this.plantMissingFailCount = 0; + + %this.undoGroup = new SimSet(); + ND_ServerGroup.add(%this.undoGroup); + + //Reset mirror error list + %client = %this.client; + + %countX = $NS[%client, "MXC"]; + %countZ = $NS[%client, "MZC"]; + + for(%i = 0; %i < %countX; %i++) + $NS[%client, "MXK", $NS[%client, "MXE", %i]] = ""; + + for(%i = 0; %i < %countZ; %i++) + $NS[%client, "MZK", $NS[%client, "MZE", %i]] = ""; + + $NS[%client, "MZC"] = 0; + $NS[%client, "MXC"] = 0; + + //Make list of spawned clients to scope bricks + %this.numClients = 0; + + for(%i = 0; %i < ClientGroup.getCount(); %i++) + { + %cl = ClientGroup.getObject(%i); + + if(%cl.hasSpawnedOnce + && isObject(%obj = %cl.getControlObject()) + && vectorDist(%this.ghostPosition, %obj.getTransform()) < 10000) + { + $NS[%this, "CL", %this.numClients] = %cl; + %this.numClients++; + } + } + + if($Pref::Server::ND::PlayMenuSounds && %this.brickCount > $Pref::Server::ND::ProcessPerTick * 10) + messageClient(%this.client, 'MsgUploadStart', ""); + + %this.tickPlantSearch($Pref::Server::ND::ProcessPerTick, %position, %angleID); +} + +//Go through the list of bricks until we find one that plants successfully +function ND_Selection::tickPlantSearch(%this, %remainingPlants, %position, %angleID) +{ + %start = %this.plantSearchIndex; + %end = %start + %remainingPlants; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + %client = %this.client; + + if(isObject(%this.targetGroup)) + { + %group = %this.targetGroup; + %bl_id = %this.targetBlid; + } + else + { + %group = %client.brickGroup.getId(); + %bl_id = %client.bl_id; + } + + %qCount = %this.plantQueueCount; + %numClients = %this.numClients; + + for(%i = %start; %i < %end; %i++) + { + //Brick already placed + if($NP[%this, %i]) + continue; + + //Skip nonexistant bricks + if($NS[%this, "D", %i] == 0) + { + $NP[%this, %i] = true; + %this.plantMissingFailCount++; + continue; + } + + //Attempt to place brick + %brick = ND_Selection::plantBrick(%this, %i, %position, %angleID, %group, %client, %bl_id); + %plants++; + + if(%brick > 0) + { + //Success! Add connected bricks to plant queue + %this.plantSuccessCount++; + %this.undoGroup.add(%brick); + + $NP[%this, %i] = true; + + %conns = $NS[%this, "N", %i]; + for(%j = 0; %j < %conns; %j++) + { + %id = $NS[%this, "C", %i, %j]; + + if(%id < %i && !$NP[%this, %id]) + { + %found = true; + + $NS[%this, "PQueue", %qCount] = %id; + $NP[%this, %id] = true; + %qCount++; + } + } + + //Instantly ghost the brick to all spawned clients (wow hacks) + for(%j = 0; %j < %numClients; %j++) + { + %cl = $NS[%this, "CL", %j]; + %brick.scopeToClient(%cl); + %brick.clearScopeToClient(%cl); + } + + //If we added bricks to plant queue, switch to second loop + if(%found) + { + %this.plantSearchIndex = %i + 1; + %this.plantQueueCount = %qCount; + %this.tickPlantTree(%remainingPlants - %plants, %position, %angleID); + return; + } + + %lastPos = %brick.position; + } + else if(%brick == -1) + { + $NP[%this, %i] = true; + %this.plantBlockedFailCount++; + } + else if(%brick == -2) + { + $NP[%this, %i] = true; + %this.plantTrustFailCount++; + } + } + + %this.plantSearchIndex = %i; + %this.plantQueueCount = %qCount; + + if(strLen(%lastPos)) + serverPlay3D(BrickPlantSound, %lastPos); + + //Tell the client how far we got + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + if(%end < %this.brickCount && %this.plantSuccessCount < %this.brickCount) + %this.plantSchedule = %this.schedule(30, tickPlantSearch, $Pref::Server::ND::ProcessPerTick, %position, %angleID); + else + %this.finishPlant(); +} + +//Plant search has prepared a queue, plant all bricks in this queue and add their connected bricks aswell +function ND_Selection::tickPlantTree(%this, %remainingPlants, %position, %angleID) +{ + %start = %this.plantQueueIndex; + %end = %start + %remainingPlants; + + %client = %this.client; + + if(isObject(%this.targetGroup)) + { + %group = %this.targetGroup; + %bl_id = %this.targetBlid; + } + else + { + %group = %client.brickGroup.getId(); + %bl_id = %client.bl_id; + } + + %qCount = %this.plantQueueCount; + %numClients = %this.numClients; + + %searchIndex = %this.plantSearchIndex; + + for(%i = %start; %i < %end; %i++) + { + //The queue is empty! Switch back to plant search. + if(%i >= %qCount) + { + if(strLen(%lastPos)) + serverPlay3D(BrickPlantSound, %lastPos); + + %this.plantQueueCount = %qCount; + %this.plantQueueIndex = %i; + %this.tickPlantSearch(%end - %i, %position, %angleID); + return; + } + + //Attempt to plant queued brick + %bId = $NS[%this, "PQueue", %i]; + + //Skip nonexistant bricks + if($NS[%this, "D", %i] == 0) + { + $NP[%this, %bId] = true; + %this.plantMissingFailCount++; + continue; + } + + %brick = ND_Selection::plantBrick(%this, %bId, %position, %angleID, %group, %client, %bl_id); + + if(%brick > 0) + { + //Success! Add connected bricks to plant queue + %this.plantSuccessCount++; + %this.undoGroup.add(%brick); + + $NP[%this, %bId] = true; + + %conns = $NS[%this, "N", %bId]; + for(%j = 0; %j < %conns; %j++) + { + %id = $NS[%this, "C", %bId, %j]; + + if(%id < %searchIndex && !$NP[%this, %id]) + { + $NS[%this, "PQueue", %qCount] = %id; + $NP[%this, %id] = true; + %qCount++; + } + } + + %lastPos = %brick.position; + + //Instantly ghost the brick to all spawned clients (wow hacks) + for(%j = 0; %j < %numClients; %j++) + { + %cl = $NS[%this, "CL", %j]; + %brick.scopeToClient(%cl); + %brick.clearScopeToClient(%cl); + } + } + else if(%brick == -1) + { + %this.plantBlockedFailCount++; + $NP[%this, %bId] = true; + } + else if(%brick == -2) + { + %this.plantTrustFailCount++; + $NP[%this, %bId] = true; + } + } + + if(strLen(%lastPos)) + serverPlay3D(BrickPlantSound, %lastPos); + + //Tell the client how far we got + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + %this.plantQueueCount = %qCount; + %this.plantQueueIndex = %i; + + %this.plantSchedule = %this.schedule(30, tickPlantTree, $Pref::Server::ND::ProcessPerTick, %position, %angleID); +} + +//Attempt to plant brick with id %i +//Returns brick if planted, 0 if floating, -1 if blocked, -2 if trust failure +function ND_Selection::plantBrick(%this, %i, %position, %angleID, %brickGroup, %client, %bl_id) +{ + //Offset position + %bPos = $NS[%this, "P", %i]; + + //Local angle id + %bAngle = $NS[%this, "R", %i]; + + //Apply mirror effects (ugh) + %datablock = $NS[%this, "D", %i]; + + %mirrX = %this.ghostMirrorX; + %mirrY = %this.ghostMirrorY; + %mirrZ = %this.ghostMirrorZ; + + if(%mirrX) + { + //Mirror offset + %bPos = -firstWord(%bPos) SPC restWords(%bPos); + + //Handle symmetries + switch($ND::Symmetry[%datablock]) + { + //Asymmetric + case 0: + if(%db = $ND::SymmetryXDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryXOffset[%datablock]) % 4; + + //Pair is made on X, so apply mirror logic for X afterwards + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + } + else + { + //Add datablock to list of mirror problems + if(!$NS[%client, "MXK", %datablock]) + { + %id = $NS[%client, "MXC"]; + $NS[%client, "MXC"]++; + + $NS[%client, "MXE", %id] = %datablock; + $NS[%client, "MXK", %datablock] = true; + } + } + + //Do nothing for fully symmetric + + //X symmetric - rotate 180 degrees if brick is angled 90 or 270 degrees + case 2: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + + //Y symmetric - rotate 180 degrees if brick is angled 0 or 180 degrees + case 3: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + + //X+Y symmetric - rotate 90 degrees + case 4: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 1) % 4; + else + %bAngle = (%bAngle + 3) % 4; + + //X-Y symmetric - rotate -90 degrees + case 5: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 3) % 4; + else + %bAngle = (%bAngle + 1) % 4; + } + } + else if(%mirrY) + { + //Mirror offset + %bPos = getWord(%bPos, 0) SPC -getWord(%bPos, 1) SPC getWord(%bPos, 2); + + //Handle symmetries + switch($ND::Symmetry[%datablock]) + { + //Asymmetric + case 0: + if(%db = $ND::SymmetryXDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryXOffset[%datablock]) % 4; + + //Pair is made on X, so apply mirror logic for X afterwards + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + } + else + { + //Add datablock to list of mirror problems + if(!$NS[%client, "MXK", %datablock]) + { + %id = $NS[%client, "MXC"]; + $NS[%client, "MXC"]++; + + $NS[%client, "MXE", %id]= %datablock; + $NS[%client, "MXK", %datablock] = true; + } + } + + //Do nothing for fully symmetric + + //X symmetric - rotate 180 degrees if brick is angled 90 or 270 degrees + case 2: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + + //Y symmetric - rotate 180 degrees if brick is angled 0 or 180 degrees + case 3: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + + //X+Y symmetric - rotate 90 degrees + case 4: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 1) % 4; + else + %bAngle = (%bAngle + 3) % 4; + + //X-Y symmetric - rotate -90 degrees + case 5: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 3) % 4; + else + %bAngle = (%bAngle + 1) % 4; + } + } + + if(%mirrZ) + { + //Mirror offset + %bPos = getWords(%bPos, 0, 1) SPC -getWord(%bPos, 2); + + //Change datablock if asymmetric + if(!$ND::SymmetryZ[%datablock]) + { + if(%db = $ND::SymmetryZDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryZOffset[%datablock]) % 4; + } + else + { + //Add datablock to list of mirror problems + if(!$NS[%client, "MZK", %datablock]) + { + %id = $NS[%client, "MZC"]; + $NS[%client, "MZC"]++; + + $NS[%client, "MZE", %id]= %datablock; + $NS[%client, "MZK", %datablock] = true; + } + } + } + } + + //Rotate and add offset + %bAngle = (%bAngle + %angleID) % 4; + %bPos = vectorAdd(%position, ndRotateVector(%bPos, %angleID)); + + switch(%bAngle) + { + case 0: %bRot = "1 0 0 0"; + case 1: %bRot = "0 0 1 90.0002"; + case 2: %bRot = "0 0 1 180"; + case 3: %bRot = "0 0 -1 90.0002"; + } + + //Attempt to plant brick + %brick = new FxDTSBrick() + { + datablock = %datablock; + isPlanted = true; + client = %client; + + position = %bPos; + rotation = %bRot; + angleID = %bAngle; + + colorID = $NS[%this, "CO", %i]; + colorFxID = $NS[%this, "CF", %i]; + + printID = $NS[%this, "PR", %i]; + }; + + //This will call ::onLoadPlant instead of ::onPlant + %prev1 = $Server_LoadFileObj; + %prev2 = $LastLoadedBrick; + $Server_LoadFileObj = %brick; + $LastLoadedBrick = %brick; + + //Add to brickgroup + %brickGroup.add(%brick); + + //Attempt plant + %error = %brick.plant(); + + //Restore variable + $Server_LoadFileObj = %prev1; + $LastLoadedBrick = %prev2; + + if(!isObject(%brick)) + return -1; + + if(%error == 2) + { + //Do we plant floating bricks? + if(%this.forcePlant) + { + //Brick is floating. Pretend it is supported by terrain + %brick.isBaseplate = true; + + //Make engine recompute distance from ground to apply it + %brick.willCauseChainKill(); + } + else + { + %brick.delete(); + return 0; + } + } + else if(%error) + { + %brick.delete(); + return -1; + } + + //Check for trust + %downCount = %brick.getNumDownBricks(); + + if(!%client.isAdmin || !$Pref::Server::ND::AdminTrustBypass2) + { + for(%j = 0; %j < %downCount; %j++) + { + if(!ndFastTrustCheck(%brick.getDownBrick(%j), %bl_id, %brickGroup)) + { + %brick.delete(); + return -2; + } + } + + %upCount = %brick.getNumUpBricks(); + + for(%j = 0; %j < %upCount; %j++) + { + if(!ndFastTrustCheck(%brick.getUpBrick(%j), %bl_id, %brickGroup)) + { + %brick.delete(); + return -2; + } + } + } + else if(!%downCount) + %upCount = %brick.getNumUpBricks(); + + //Finished trust check + if(%downCount) + %brick.stackBL_ID = %brick.getDownBrick(0).stackBL_ID; + else if(%upCount) + %brick.stackBL_ID = %brick.getUpBrick(0).stackBL_ID; + else + %brick.stackBL_ID = %bl_id; + + %brick.trustCheckFinished(); + + //Apply special settings + %brick.setRendering(!$NS[%this, "NR", %i]); + %brick.setRaycasting(!$NS[%this, "NRC", %i]); + %brick.setColliding(!$NS[%this, "NC", %i]); + %brick.setShapeFx($NS[%this, "SF", %i]); + + //Apply events + if(%numEvents = $NS[%this, "EN", %i]) + { + %brick.numEvents = %numEvents; + %brick.implicitCancelEvents = 0; + + for(%j = 0; %j < %numEvents; %j++) + { + %brick.eventEnabled[%j] = $NS[%this, "EE", %i, %j]; + %brick.eventDelay[%j] = $NS[%this, "ED", %i, %j]; + + %inputIdx = $NS[%this, "EII", %i, %j]; + + %brick.eventInput[%j] = $NS[%this, "EI", %i, %j]; + %brick.eventInputIdx[%j] = %inputIdx; + + %target = $NS[%this, "ET", %i, %j]; + %targetIdx = $NS[%this, "ETI", %i, %j]; + + if(%targetIdx == -1) + { + %nt = $NS[%this, "ENT", %i, %j]; + %brick.eventNT[%j] = %nt; + } + + %brick.eventTarget[%j] = %target; + %brick.eventTargetIdx[%j] = %targetIdx; + + %output = $NS[%this, "EO", %i, %j]; + %outputIdx = $NS[%this, "EOI", %i, %j]; + + //Only rotate outputs for named bricks if they are selected + if(%targetIdx >= 0 || $NS[%this, "HN", %nt]) + { + //Rotate fireRelay events + switch$(%output) + { + case "fireRelayUp": %dir = 0; + case "fireRelayDown": %dir = 1; + case "fireRelayNorth": %dir = 2; + case "fireRelayEast": %dir = 3; + case "fireRelaySouth": %dir = 4; + case "fireRelayWest": %dir = 5; + default: %dir = -1; + } + + if(%dir >= 0) + { + %rotated = ndTransformDirection(%dir, %angleID, %mirrX, %mirrY, %mirrZ); + %outputIdx += %rotated - %dir; + + switch(%rotated) + { + case 0: %output = "fireRelayUp"; + case 1: %output = "fireRelayDown"; + case 2: %output = "fireRelayNorth"; + case 3: %output = "fireRelayEast"; + case 4: %output = "fireRelaySouth"; + case 5: %output = "fireRelayWest"; + } + } + } + + %brick.eventOutput[%j] = %output; + %brick.eventOutputIdx[%j] = %outputIdx; + %brick.eventOutputAppendClient[%j] = $NS[%this, "EOC", %i, %j]; + + //Why does this need to be so complicated? + if(%targetIdx >= 0) + %targetClass = getWord($InputEvent_TargetListfxDtsBrick_[%inputIdx], %targetIdx * 2 + 1); + else + %targetClass = "FxDTSBrick"; + + %paramList = $OutputEvent_ParameterList[%targetClass, %outputIdx]; + %paramCount = getFieldCount(%paramList); + + for(%k = 0; %k < %paramCount; %k++) + { + %param = $NS[%this, "EP", %i, %j, %k]; + + //Only rotate outputs for named bricks if they are selected + if(%targetIdx >= 0 || $NS[%this, "HN", %nt]) + { + %paramType = getField(%paramList, %k); + + switch$(getWord(%paramType, 0)) + { + case "vector": + //Apply mirror effects + if(%mirrX) + %param = -firstWord(%param) SPC restWords(%param); + else if(%mirrY) + %param = getWord(%param, 0) SPC -getWord(%param, 1) SPC getWord(%param, 2); + + if(%mirrZ) + %param = getWord(%param, 0) SPC getWord(%param, 1) SPC -getWord(%param, 2); + + %param = ndRotateVector(%param, %angleID); + + case "list": + %value = getWord(%paramType, %param * 2 + 1); + + switch$(%value) + { + case "Up": %dir = 0; + case "Down": %dir = 1; + case "North": %dir = 2; + case "East": %dir = 3; + case "South": %dir = 4; + case "West": %dir = 5; + default: %dir = -1; + } + + if(%dir >= 0) + { + switch(ndTransformDirection(%dir, %angleID, %mirrX, %mirrY, %mirrZ)) + { + case 0: %value = "Up"; + case 1: %value = "Down"; + case 2: %value = "North"; + case 3: %value = "East"; + case 4: %value = "South"; + case 5: %value = "West"; + } + + for(%l = 1; %l < getWordCount(%paramType); %l += 2) + { + if(getWord(%paramType, %l) $= %value) + { + %param = getWord(%paramType, %l + 1); + break; + } + } + } + } + } + + %brick.eventOutputParameter[%j, %k + 1] = %param; + } + } + } + + setCurrentQuotaObject(getQuotaObjectFromClient(%client)); + + if((%tmp = $NS[%this, "NT", %i]) !$= "") + %brick.setNTObjectName(%tmp); + + if(%tmp = $NS[%this, "LD", %i]) + %brick.setLight(%tmp, %client); + + if(%tmp = $NS[%this, "ED", %i]) + { + %dir = ndTransformDirection($NS[%this, "ER", %i], %angleID, %mirrX, %mirrY, %mirrZ); + + %brick.emitterDirection = %dir; + %brick.setEmitter(%tmp, %client); + } + + if(%tmp = $NS[%this, "ID", %i]) + { + %pos = ndTransformDirection($NS[%this, "IP", %i], %angleID, %mirrX, %mirrY, %mirrZ); + %dir = ndTransformDirection($NS[%this, "IR", %i], %angleID, %mirrX, %mirrY, %mirrZ); + + %brick.itemPosition = %pos; + %brick.itemDirection = %dir; + %brick.itemRespawnTime = $NS[%this, "IT", %i]; + %brick.setItem(%tmp, %client); + } + + if(%tmp = $NS[%this, "VD", %i]) + { + %brick.reColorVehicle = $NS[%this, "VC", %i]; + %brick.setVehicle(%tmp, %client); + } + + if(%tmp = $NS[%this, "MD", %i]) + %brick.setSound(%tmp, %client); + + return %brick; +} + +//Finished planting all the bricks! +function ND_Selection::finishPlant(%this) +{ + //Report mirror errors + if($NS[%this.client, "MXC"] > 0 || $NS[%this.client, "MZC"] > 0) + messageClient(%this.client, '', "\c6Some bricks were probably mirrored incorrectly. Say \c3/mirErrors\c6 to find out more."); + + %count = %this.brickCount; + %planted = %this.plantSuccessCount; + %blocked = %this.plantBlockedFailCount; + %trusted = %this.plantTrustFailCount; + %missing = %this.plantMissingFailCount; + %floating = %count - %planted - %blocked - %trusted - %missing; + + %s = %this.plantSuccessCount == 1 ? "" : "s"; + %message = "\c6Planted \c3" @ %this.plantSuccessCount @ "\c6 / \c3" @ %count @ "\c6 Brick" @ %s @ "!"; + + if(%trusted) + %message = %message @ "\n\c3" @ %trusted @ "\c6 missing trust."; + + if(%blocked) + %message = %message @ "\n\c3" @ %blocked @ "\c6 blocked."; + + if(%floating) + %message = %message @ "\n\c3" @ %floating @ "\c6 floating."; + + if(%missing) + %message = %message @ "\n\c3" @ %missing @ "\c6 missing Datablock."; + + commandToClient(%this.client, 'centerPrint', %message, 4); + + if($Pref::Server::ND::PlayMenuSounds && %planted && %this.brickCount > $Pref::Server::ND::ProcessPerTick * 10) + messageClient(%this.client, 'MsgProcessComplete', ""); + + deleteVariables("$NP" @ %this @ "_*"); + + if(%planted) + { + %this.undoGroup.brickCount = %this.undoGroup.getCount(); + %this.client.undoStack.push(%this.undoGroup TAB "ND_PLANT"); + } + else + %this.undoGroup.delete(); + + %this.client.ndSetMode(NDM_PlantCopy); +} + +//Cancel planting bricks +function ND_Selection::cancelPlanting(%this) +{ + cancel(%this.plantSchedule); + deleteVariables("$NP" @ %this @ "_*"); + + if(%this.plantSuccessCount) + { + %this.undoGroup.brickCount = %this.undoGroup.getCount(); + %this.client.undoStack.push(%this.undoGroup TAB "ND_PLANT"); + } + else + %this.undoGroup.delete(); +} + + + +//Fill Colors +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Start filling bricks with a specific color +function ND_Selection::startFillColor(%this, %mode, %colorID) +{ + %this.paintIndex = 0; + %this.paintFailCount = 0; + %this.paintSuccessCount = 0; + + //Create undo group + %this.undoGroup = new ScriptObject(ND_UndoGroupPaint) + { + paintType = %mode; + brickCount = 0; + client = %this.client; + }; + + ND_ServerGroup.add(%this.undoGroup); + + %this.tickFillColor(%mode, %colorID); +} + +//Tick filling bricks with a specific color +function ND_Selection::tickFillColor(%this, %mode, %colorID) +{ + cancel(%this.fillColorSchedule); + + %start = %this.paintIndex; + %end = %start + $Pref::Server::ND::ProcessPerTick; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + %admin = %this.client.isAdmin; + %group2 = %this.client.brickGroup.getId(); + %bl_id = %this.client.bl_id; + + %paintCount = %this.paintSuccessCount; + %failCount = %this.paintFailCount; + + %undoCount = %this.undoGroup.brickCount; + + %clientId = %this.client; + %undoId = %this.undoGroup; + + for(%i = %start; %i < %end; %i++) + { + if(isObject(%brick = $NS[%this, "B", %i])) + { + if(ndTrustCheckModify(%brick, %group2, %bl_id, %admin)) + { + //Color brick + switch(%mode) + { + case 0: + //Don't change to same value + if(%brick.colorID == %colorID) + continue; + + //Write previous value to undo array + $NU[%clientId, %undoId, "V", %paintCount] = %brick.colorID; + + %brick.setColor(%colorID); + + //Update selection data + $NS[%this, "CO", $NS[%this, "I", %brick]] = %colorID; + + case 1: + //Check whether brick is highlighted + if($NDHN[%brick]) + { + //Don't change to same value + if($NDHF[%brick] == %colorID) + continue; + + //Write previous value to undo array + $NU[%clientId, %undoId, "V", %paintCount] = $NDHF[%brick]; + + //If we're highlighted, change the original color instead + $NDHF[%brick] = %colorID; + } + else + { + //Don't change to same value + if(%brick.colorFxID == %colorID) + continue; + + //Write previous value to undo array + $NU[%clientId, %undoId, "V", %paintCount] = %brick.colorFxID; + + %brick.setColorFx(%colorID); + } + + //Update selection data + $NS[%this, "CF", $NS[%this, "I", %brick]] = %colorID; + + case 2: + //Don't change to same value + if(%brick.shapeFxID == %colorID) + continue; + + //Write previous value to undo array + $NU[%clientId, %undoId, "V", %paintCount] = %brick.shapeFxID; + + %brick.setShapeFx(%colorID); + + //Update selection data + $NS[%this, "SF", $NS[%this, "I", %brick]] = %colorID; + } + + $NU[%clientId, %undoId, "B", %paintCount] = %brick; + %paintCount++; + } + else + %failCount++; + } + } + + %this.paintIndex = %i; + %this.paintSuccessCount = %paintCount; + %this.paintFailCount = %failCount; + + %this.undoGroup.brickCount = %paintCount; + + //Tell the client how much we painted this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + if(%i >= %this.brickCount) + %this.finishFillColor(); + else + %this.fillColorSchedule = %this.schedule(30, tickFillColor, %mode, %colorID); +} + +//Finish filling color +function ND_Selection::finishFillColor(%this) +{ + %s = %this.undoGroup.brickCount == 1 ? "" : "s"; + %msg = "\c6Painted \c3" @ %this.undoGroup.brickCount @ "\c6 Brick" @ %s @ "!"; + + if(%this.paintFailCount > 0) + %msg = %msg @ "\n\c3" @ %this.paintFailCount @ "\c6 missing trust."; + + commandToClient(%this.client, 'centerPrint', %msg, 8); + + if(%this.undoGroup.brickCount) + %this.client.undoStack.push(%this.undoGroup TAB "ND_PAINT"); + else + %this.undoGroup.delete(); + + %this.client.ndSetMode(NDM_FillColor); +} + +//Cancel filling color +function ND_Selection::cancelFillColor(%this) +{ + cancel(%this.fillColorSchedule); + + if(%this.undoGroup.brickCount) + %this.client.undoStack.push(%this.undoGroup TAB "ND_PAINT"); + else + %this.undoGroup.delete(); +} + + + +//Fill Wrench +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Start applying wrench settings to all bricks +function ND_Selection::startFillWrench(%this, %data) +{ + %valid = false; + %this.fillWrenchName = false; + %this.fillWrenchLight = false; + %this.fillWrenchEmitter = false; + %this.fillWrenchEmitterDir = false; + %this.fillWrenchItem = false; + %this.fillWrenchItemPos = false; + %this.fillWrenchItemDir = false; + %this.fillWrenchItemTime = false; + %this.fillWrenchRaycasting = false; + %this.fillWrenchCollision = false; + %this.fillWrenchRendering = false; + + //Verify and save data + %fieldCount = getFieldCount(%data); + + for(%i = 0; %i < %fieldCount; %i++) + { + %field = getField(%data, %i); + + %type = getWord(%field, 0); + %value = trim(restWords(%field)); + + switch$(%type) + { + case "N": + %this.fillWrenchName = true; + %this.fillWrenchNameValue = getSafeVariableName(%value); + %valid = true; + + case "LDB": + if((isObject(%value) && %value.getClassName() $= "FxLightData" && %value.uiName !$= "") || %value == 0) + { + %this.fillWrenchLight = true; + %this.fillWrenchLightValue = %value; + %valid = true; + } + else + messageClient(%this.client, '', "\c6Fill wrench error - Invalid light datablock " @ %value); + + case "EDB": + if((isObject(%value) && %value.getClassName() $= "ParticleEmitterData" && %value.uiName !$= "") || %value == 0) + { + %this.fillWrenchEmitter = true; + %this.fillWrenchEmitterValue = %value; + %valid = true; + } + else + messageClient(%this.client, '', "\c6Fill wrench error - Invalid emitter datablock " @ %value); + + case "EDIR": + if(%value >= 0 && %value <= 5) + { + %this.fillWrenchEmitterDir = true; + %this.fillWrenchEmitterDirValue = %value; + %valid = true; + } + else + messageClient(%this.client, '', "\c6Fill wrench error - Invalid emitter direction " @ %value); + + case "IDB": + if((isObject(%value) && %value.getClassName() $= "ItemData" && %value.uiName !$= "") || %value == 0) + { + %this.fillWrenchItem = true; + %this.fillWrenchItemValue = %value; + %valid = true; + } + else + messageClient(%this.client, '', "\c6Fill wrench error - Invalid item datablock " @ %value); + + case "IPOS": + if(%value >= 0 && %value <= 5) + { + %this.fillWrenchItemPos = true; + %this.fillWrenchItemPosValue = %value; + %valid = true; + } + else + messageClient(%this.client, '', "\c6Fill wrench error - Invalid item position " @ %value); + + case "IDIR": + if(%value >= 2 && %value <= 5) + { + %this.fillWrenchItemDir = true; + %this.fillWrenchItemDirValue = %value; + %valid = true; + } + else + messageClient(%this.client, '', "\c6Fill wrench error - Invalid item direction " @ %value); + + case "IRT": + %this.fillWrenchItemTime = true; + %this.fillWrenchItemTimeValue = mFloor(%value) * 1000; + %valid = true; + + case "RC": + %this.fillWrenchRaycasting = true; + %this.fillWrenchRaycastingValue = %value; + %valid = true; + + case "C": + %this.fillWrenchCollision = true; + %this.fillWrenchCollisionValue = %value; + %valid = true; + + case "R": + %this.fillWrenchRendering = true; + %this.fillWrenchRenderingValue = %value; + %valid = true; + + default: + messageClient(%this.client, '', "\c6Fill wrench error - Invalid field " @ %type); + } + } + + if(!%valid) + { + messageClient(%this.client, '', "\c6Fill wrench error - No data to apply?"); + %this.cancelFillWrench(); + %this.client.ndSetMode(%this.client.ndLastSelectMode); + return; + } + + %this.wrenchIndex = 0; + %this.wrenchFailCount = 0; + %this.wrenchSuccessCount = 0; + + //Create undo group + %this.undoGroup = new ScriptObject(ND_UndoGroupWrench) + { + fillWrenchName = %this.fillWrenchName; + fillWrenchLight = %this.fillWrenchLight; + fillWrenchEmitter = %this.fillWrenchEmitter; + fillWrenchEmitterDir = %this.fillWrenchEmitterDir; + fillWrenchItem = %this.fillWrenchItem; + fillWrenchItemPos = %this.fillWrenchItemPos; + fillWrenchItemDir = %this.fillWrenchItemDir; + fillWrenchItemTime = %this.fillWrenchItemTime; + fillWrenchRaycasting = %this.fillWrenchRaycasting; + fillWrenchCollision = %this.fillWrenchCollision; + fillWrenchRendering = %this.fillWrenchRendering; + + brickCount = 0; + client = %this.client; + }; + + ND_ServerGroup.add(%this.undoGroup); + + %this.tickFillWrench(); +} + +//Tick applying wrench settings to all bricks +function ND_Selection::tickFillWrench(%this) +{ + cancel(%this.fillWrenchSchedule); + + %start = %this.wrenchIndex; + %end = %start + $Pref::Server::ND::ProcessPerTick; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + %client = %this.client; + + %admin = %this.client.isAdmin; + %group2 = %client.brickGroup.getId(); + %bl_id = %client.bl_id; + + %wrenchCount = %this.wrenchSuccessCount; + %failCount = %this.wrenchFailCount; + + %undoCount = %this.undoGroup.brickCount; + + %clientId = %this.client; + %undoId = %this.undoGroup; + + setCurrentQuotaObject(getQuotaObjectFromClient(%client)); + + %fillWrenchName = %this.fillWrenchName; + %fillWrenchLight = %this.fillWrenchLight; + %fillWrenchEmitter = %this.fillWrenchEmitter; + %fillWrenchEmitterDir = %this.fillWrenchEmitterDir; + %fillWrenchItem = %this.fillWrenchItem; + %fillWrenchItemPos = %this.fillWrenchItemPos; + %fillWrenchItemDir = %this.fillWrenchItemDir; + %fillWrenchItemTime = %this.fillWrenchItemTime; + %fillWrenchRaycasting = %this.fillWrenchRaycasting; + %fillWrenchCollision = %this.fillWrenchCollision; + %fillWrenchRendering = %this.fillWrenchRendering; + + %fillWrenchNameValue = %this.fillWrenchNameValue; + %fillWrenchLightValue = %this.fillWrenchLightValue; + %fillWrenchEmitterValue = %this.fillWrenchEmitterValue; + %fillWrenchEmitterDirValue = %this.fillWrenchEmitterDirValue; + %fillWrenchItemValue = %this.fillWrenchItemValue; + %fillWrenchItemPosValue = %this.fillWrenchItemPosValue; + %fillWrenchItemDirValue = %this.fillWrenchItemDirValue; + %fillWrenchItemTimeValue = %this.fillWrenchItemTimeValue; + %fillWrenchRaycastingValue = %this.fillWrenchRaycastingValue; + %fillWrenchCollisionValue = %this.fillWrenchCollisionValue; + %fillWrenchRenderingValue = %this.fillWrenchRenderingValue; + + for(%i = %start; %i < %end; %i++) + { + if(isObject(%brick = $NS[%this, "B", %i])) + { + if(ndTrustCheckModify(%brick, %group2, %bl_id, %admin)) + { + %undoRequired = false; + + //Apply wrench settings + if(%fillWrenchName) + { + %curr = getSubStr(%brick.getName(), 1, 254); + $NU[%clientId, %undoId, "N", %undoCount] = %curr; + + if(%curr !$= %fillWrenchNameValue) + { + %brick.setNTObjectName(%fillWrenchNameValue); + %undoRequired = true; + } + } + + if(%fillWrenchLight) + { + if(%tmp = %brick.light | 0) + %curr = %tmp.getDatablock(); + else + %curr = 0; + + $NU[%clientId, %undoId, "LDB", %undoCount] = %curr; + + if(%curr != %fillWrenchLightValue) + { + %brick.setLight(%fillWrenchLightValue, %client); + %undoRequired = true; + } + } + + if(%fillWrenchEmitter) + { + if(%tmp = %brick.emitter | 0) + %curr = %tmp.getEmitterDatablock(); + else if(%tmp = %brick.oldEmitterDB | 0) + %curr = %tmp; + else + %curr = 0; + + $NU[%clientId, %undoId, "EDB", %undoCount] = %curr; + + if(%curr != %fillWrenchEmitterValue) + { + %brick.setEmitter(%fillWrenchEmitterValue, %client); + %undoRequired = true; + } + } + + if(%fillWrenchEmitterDir) + { + %curr = %brick.emitterDirection; + $NU[%clientId, %undoId, "EDIR", %undoCount] = %curr; + + if(%curr != %fillWrenchEmitterDirValue) + { + %brick.setEmitterDirection(%fillWrenchEmitterDirValue); + %undoRequired = true; + } + } + + if(%fillWrenchItem) + { + if(%tmp = %brick.item | 0) + %curr = %tmp.getDatablock(); + else + %curr = 0; + + $NU[%clientId, %undoId, "IDB", %undoCount] = %curr; + + if(%curr != %fillWrenchItemValue) + { + %brick.setItem(%fillWrenchItemValue, %client); + %undoRequired = true; + } + } + + if(%fillWrenchItemPos) + { + %curr = %brick.itemPosition; + $NU[%clientId, %undoId, "IPOS", %undoCount] = %curr; + + if(%curr != %fillWrenchItemPosValue) + { + %brick.setItemPosition(%fillWrenchItemPosValue); + %undoRequired = true; + } + } + + if(%fillWrenchItemDir) + { + %curr = %brick.itemPosition; + $NU[%clientId, %undoId, "IDIR", %undoCount] = %curr; + + if(%curr != %fillWrenchItemDirValue) + { + %brick.setItemDirection(%fillWrenchItemDirValue); + %undoRequired = true; + } + } + + if(%fillWrenchItemTime) + { + %curr = %brick.itemRespawnTime; + $NU[%clientId, %undoId, "IRT", %undoCount] = %curr; + + if(%curr != %fillWrenchItemTimeValue) + { + %brick.setItemRespawnTime(%fillWrenchItemTimeValue); + %undoRequired = true; + } + } + + if(%fillWrenchRaycasting) + { + %curr = %brick.isRaycasting(); + $NU[%clientId, %undoId, "RC", %undoCount] = %curr; + + if(%curr != %fillWrenchRaycastingValue) + { + %brick.setRaycasting(%fillWrenchRaycastingValue); + %undoRequired = true; + } + } + + if(%fillWrenchCollision) + { + %curr = %brick.isColliding(); + $NU[%clientId, %undoId, "C", %undoCount] = %curr; + + if(%curr != %fillWrenchCollisionValue) + { + %brick.setColliding(%fillWrenchCollisionValue); + %undoRequired = true; + } + } + + if(%fillWrenchRendering) + { + %curr = %brick.isRendering(); + $NU[%clientId, %undoId, "R", %undoCount] = %curr; + + if(%curr != %fillWrenchRenderingValue) + { + //Copy emitter ...? + if(!%fillWrenchRenderingValue && (%tmp = %brick.emitter | 0)) + %emitter = %tmp.getEmitterDatablock(); + else + %emitter = 0; + + %brick.setRendering(%fillWrenchRenderingValue); + %undoRequired = true; + + if(!%fillWrenchRenderingValue && %emitter) + %brick.setEmitter(%emitter); + } + } + + if(%undoRequired) + { + $NU[%clientId, %undoId, "B", %undoCount] = %brick; + %undoCount++; + } + + %wrenchCount++; + } + else + %failCount++; + } + } + + clearCurrentQuotaObject(); + + %this.wrenchIndex = %i; + %this.wrenchSuccessCount = %wrenchCount; + %this.wrenchFailCount = %failCount; + + %this.undoGroup.brickCount = %undoCount; + + //Tell the client how much we wrenched this tick + if(%client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %client.ndUpdateBottomPrint(); + %client.ndLastMessageTime = $Sim::Time; + } + + if(%i >= %this.brickCount) + %this.finishFillWrench(); + else + %this.fillWrenchSchedule = %this.schedule(30, tickFillWrench); +} + +//Finish wrenching +function ND_Selection::finishFillWrench(%this) +{ + %s = %this.undoGroup.brickCount == 1 ? "" : "s"; + %msg = "\c6Applied changes to \c3" @ %this.undoGroup.brickCount @ "\c6 Brick" @ %s @ "!"; + + if(%this.wrenchFailCount > 0) + %msg = %msg @ "\n\c3" @ %this.wrenchFailCount @ "\c6 missing trust."; + + commandToClient(%this.client, 'centerPrint', %msg, 8); + + if(%this.undoGroup.brickCount) + %this.client.undoStack.push(%this.undoGroup TAB "ND_WRENCH"); + else + %this.undoGroup.delete(); + + %this.client.ndSetMode(%this.client.ndLastSelectMode); +} + +//Cancel wrenching +function ND_Selection::cancelFillWrench(%this) +{ + cancel(%this.fillWrenchSchedule); + + if(%this.undoGroup.brickCount) + %this.client.undoStack.push(%this.undoGroup TAB "ND_WRENCH"); + else + %this.undoGroup.delete(); +} + + + +//Saving bricks +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Begin saving +function ND_Selection::startSaving(%this, %filePath) +{ + //Open file + %this.saveFilePath = %filePath; + %this.saveFile = new FileObject(); + + if(!%this.saveFile.openForWrite(%filePath)) + return false; + + //Write file header + %this.saveFile.writeLine("Do not modify this file at all. You will break it."); + %this.saveFile.writeLine("1"); + %this.saveFile.writeLine("Saved by " @ %this.client.name @ " (" @ %this.client.bl_id @ ") at " @ getDateTime()); + + //Write colorset + for(%i = 0; %i < 64; %i++) + %this.saveFile.writeLine(getColorIDTable(%i)); + + //Write line count + %this.saveFile.writeLine("Linecount " @ %this.brickCount); + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + //Schedule first tick + %this.saveStage = 0; + %this.saveIndex = 0; + %this.saveSchedule = %this.schedule(30, tickSaveBricks); + + return true; +} + +//Save some bricks +function ND_Selection::tickSaveBricks(%this) +{ + cancel(%this.saveSchedule); + + //Get bounds for this tick + %start = %this.saveIndex; + %end = %start + $Pref::Server::ND::ProcessPerTick * 2; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + %file = %this.saveFile; + + //Save bricks + for(%i = %start; %i < %end; %i++) + { + %data = $NS[%this, "D", %i]; + + //Get correct print texture + if(%data.hasPrint) + { + %fileName = getPrintTexture($NS[%this, "PR", %i]); + %fileBase = fileBase(%fileName); + %path = filePath(%fileName); + + if(%path !$= "" && %fileName !$= "base/data/shapes/bricks/brickTop.png") + { + %dirName = getSubStr(%path, 8, 999); + + %posA = strStr(%dirName, "_"); + %posB = strPos(%dirName, "_", %posA + 1); + + %aspectRatio = getSubStr(%dirName, %posA + 1, %posB - %posA - 1); + %printTexture = %aspectRatio @ "/" @ %fileBase; + } + else + %printTexture = "/"; + } + else + %printTexture = ""; + + //Write brick data + %file.writeLine(%data.uiName @ "\"" + SPC vectorAdd($NS[%this, "P", %i], %this.rootPosition) + SPC $NS[%this, "R", %i] + SPC 0 + SPC $NS[%this, "CO", %i] + SPC %printTexture + SPC $NS[%this, "CF", %i] * 1 + SPC $NS[%this, "SF", %i] * 1 + SPC !$NS[%this, "NRC", %i] + SPC !$NS[%this, "NC", %i] + SPC !$NS[%this, "NR", %i] + ); + + //Write brick name + if((%tmp = $NS[%this, "NT", %i]) !$= "") + %file.writeLine("+-NTOBJECTNAME " @ %tmp); + + //Write events + %cnt = $NS[%this, "EN", %i]; + + for(%j = 0; %j < %cnt; %j++) + { + //Basic event parameters + %enabled = $NS[%this, "EE", %i, %j]; + %inputName = $NS[%this, "EI", %i, %j]; + %delay = $NS[%this, "ED", %i, %j]; + %targetIdx = $NS[%this, "ETI", %i, %j]; + + if(%targetIdx == -1) + { + %targetName = "-1"; + %NT = $NS[%this, "ENT", %i, %j]; + } + else + { + %targetName = $NS[%this, "ET", %i, %j]; + %NT = ""; + } + + %outputName = $NS[%this, "EO", %i, %j]; + + //Temp line (without output parameters) + %line = "+-EVENT" TAB %j TAB %enabled TAB %inputName TAB %delay TAB %targetName TAB %NT TAB %outputName; + + //Output event parameters + if(%targetIdx >= 0) + %targetClass = getWord(getField($InputEvent_TargetListfxDtsBrick_[$NS[%this, "EII", %i, %j]], %targetIdx), 1); + else + %targetClass = "FxDTSBrick"; + + for(%k = 0; %k < 4; %k++) + { + %param = $NS[%this, "EP", %i, %j, %k]; + %dataType = getWord(getField($OutputEvent_parameterList[%targetClass, $NS[%this, "EOI", %i, %j]], %k), 0); + + if(%dataType $= "Datablock") + { + if(isObject(%param)) + %line = %line TAB %param.getName(); + else + %line = %line TAB "-1"; + } + else + %line = %line TAB %param; + } + + %file.writeLine(%line); + } + + //Write emitter + %edb = $NS[%this, "ED", %i]; + %edir = $NS[%this, "ER", %i]; + + if(isObject(%edb)) + %file.writeLine("+-EMITTER" SPC %edb.uiName @ "\" " @ %edir); + else if(%edir != 0) + %file.writeLine("+-EMITTER NONE\" " @ %edir); + + //Write light + %ldb = $NS[%this, "LD", %i]; + + if(isObject(%ldb)) + %file.writeLine("+-LIGHT" SPC %ldb.uiName @ "\" 1"); + + //Write item + %idb = $NS[%this, "ID", %i]; + %ipos = $NS[%this, "IP", %i]; + %idir = $NS[%this, "IR", %i]; + %irt = $NS[%this, "IT", %i]; + + if(isObject(%idb)) + %file.writeLine("+-ITEM" SPC %idb.uiName @ "\" " @ %ipos SPC %idir SPC %irt); + else if(%ipos != 0 || (%idir !$= "" && %idir != 2) || (%irt != 4000 && %irt != 0)) + %file.writeLine("+-ITEM NONE\" " @ %ipos SPC %idir SPC %irt); + + //Write music + %mdb = $NS[%this, "MD", %i]; + + if(isObject(%mdb)) + %file.writeLine("+-AUDIOEMITTER" SPC %mdb.uiName @ "\""); + + //Write vehicle + %vdb = $NS[%this, "VD", %i]; + %vcol = $NS[%this, "VC", %i]; + + if(isObject(%vdb)) + %file.writeLine("+-VEHICLE" SPC %vdb.uiName @ "\" " @ %vcol); + } + + //Save how far we got + %this.saveIndex = %i; + + //Tell the client how much we saved this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + //Finished saving all bricks? + if(%i >= %this.brickCount) + { + //Find width of connection numbers + if(%this.maxConnections >= 241 * 241) + %numberSize = 3; + else if(%this.maxConnections >= 241) + %numberSize = 2; + else + %numberSize = 1; + + //Find width of connection indices + if(%this.brickCount > 241 * 241) + %indexSize = 3; + else if(%this.brickCount > 241) + %indexSize = 2; + else + %indexSize = 1; + + //Save the sizes + %file.writeLine("ND_SIZE\" 1 " @ %this.connectionCount SPC %numberSize SPC %indexSize); + + %this.saveStage = 1; + %this.saveIndex = 0; + %this.saveLineBuffer = "ND_TREE\" "; + + //Create byte table + if(!$ND::Byte241TableCreated) + ndCreateByte241Table(); + + //Start saving connections + %this.connectionCount = 0; + %this.saveSchedule = %this.schedule(30, tickSaveConnections, %numberSize, %indexSize); + } + else + %this.saveSchedule = %this.schedule(30, tickSaveBricks); +} + +//Save some connections +function ND_Selection::tickSaveConnections(%this, %numberSize, %indexSize) +{ + cancel(%this.saveSchedule); + + //Get bounds for this tick + %start = %this.saveIndex; + %end = %start + $Pref::Server::ND::ProcessPerTick * 4; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + %file = %this.saveFile; + %connections = %this.connectionCount; + + %lineBuffer = %this.saveLineBuffer; + %len = strLen(%lineBuffer); + + //Save connections + for(%i = %start; %i < %end; %i++) + { + //Save number of connections of this brick + %cnt = $NS[%this, "N", %i]; + %connections += %cnt; + + //Write compressed connection number + if(%numberSize == 1) + { + %lineBuffer = %lineBuffer @ $ND::Byte241ToChar[%cnt]; + + %len++; + } + else if(%numberSize == 2) + { + %lineBuffer = %lineBuffer @ + $ND::Byte241ToChar[(%cnt / 241) | 0] @ + $ND::Byte241ToChar[%cnt % 241]; + + %len += 2; + } + else + { + %lineBuffer = %lineBuffer @ + $ND::Byte241ToChar[(((%cnt / 241) | 0) / 241) | 0] @ + $ND::Byte241ToChar[((%cnt / 241) | 0) % 241] @ + $ND::Byte241ToChar[%cnt % 241]; + + %len += 3; + } + + //If buffer is full, save to file + if(%len > 254) + { + %file.writeLine(%lineBuffer); + %lineBuffer = "ND_TREE\" "; + %len = 9; + } + + for(%j = 0; %j < %cnt; %j++) + { + //Write compressed connection index + if(%indexSize == 1) + { + %lineBuffer = %lineBuffer @ $ND::Byte241ToChar[$NS[%this, "C", %i, %j]]; + + %len++; + } + else if(%indexSize == 2) + { + %conn = $NS[%this, "C", %i, %j]; + + %lineBuffer = %lineBuffer @ + $ND::Byte241ToChar[(%conn / 241) | 0] @ + $ND::Byte241ToChar[%conn % 241]; + + %len += 2; + } + else + { + %conn = $NS[%this, "C", %i, %j]; + + %lineBuffer = %lineBuffer @ + $ND::Byte241ToChar[(((%conn / 241) | 0) / 241) | 0] @ + $ND::Byte241ToChar[((%conn / 241) | 0) % 241] @ + $ND::Byte241ToChar[%conn % 241]; + + %len += 3; + } + + //If buffer is full, save to file + if(%len > 254) + { + %file.writeLine(%lineBuffer); + %lineBuffer = "ND_TREE\" "; + %len = 9; + } + } + } + + //Save how far we got + %this.saveIndex = %i; + %this.saveLineBuffer = %lineBuffer; + %this.connectionCount = %connections; + + //Tell the client how much we cut this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + if(%i >= %this.brickCount) + { + if(strLen(%lineBuffer) != 9) + %file.writeLine(%lineBuffer); + + %this.saveLineBuffer = ""; + %this.finishSaving(); + } + else + %this.saveSchedule = %this.schedule(30, tickSaveConnections, %numberSize, %indexSize); +} + +//Finish saving +function ND_Selection::finishSaving(%this) +{ + %this.saveFile.close(); + %this.saveFile.delete(); + + %s1 = %this.brickCount == 1 ? "" : "s"; + %s2 = %this.connectionCount == 1 ? "" : "s"; + + messageClient(%this.client, 'MsgProcessComplete', "\c6Finished saving selection, wrote \c3" + @ %this.brickCount @ "\c6 Brick" @ %s1 @ " with \c3" @ %this.connectionCount @ "\c6 Connection" @ %s2 @ "!"); + + %this.client.ndLastSaveTime = $Sim::Time; + %this.client.ndSetMode(NDM_PlantCopy); +} + +//Cancel saving +function ND_Selection::cancelSaving(%this) +{ + cancel(%this.saveSchedule); + + %this.saveFile.close(); + %this.saveFile.delete(); + + if(isFile(%this.saveFilePath)) + fileDelete(%this.saveFilePath); + + %this.client.ndLastSaveTime = $Sim::Time; +} + + + +//Loading bricks +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//Begin loading +function ND_Selection::startLoading(%this, %filePath) +{ + //Open file + %this.loadFile = new FileObject(); + + if(!%this.loadFile.openForRead(%filePath)) + return false; + + //Skip file header + %this.loadFile.readLine(); + %cnt = %this.loadFile.readLine(); + + for(%i = 0; %i < %cnt; %i++) + %this.loadFile.readLine(); + + //Read colorset + for(%i = 0; %i < 64; %i++) + $NS[%this, "CT", %i] = ndGetClosestColorID2(getColorI(%this.loadFile.readLine())); + + //Read line count (temporary, allows displaying percentage) + %this.loadExpectedBrickCount = getWord(%this.loadFile.readLine(), 1) * 1; + + if($Pref::Server::ND::PlayMenuSounds) + messageClient(%this.client, 'MsgUploadStart', ""); + + //Schedule first tick + %this.connectionCount = 0; + %this.brickCount = 0; + %this.loadCount = 0; + + %this.loadStage = 0; + %this.loadIndex = -1; + %this.loadSchedule = %this.schedule(30, tickLoadBricks); + + return true; +} + +//Load some bricks +function ND_Selection::tickLoadBricks(%this) +{ + cancel(%this.loadSchedule); + + %file = %this.loadFile; + %index = %this.loadIndex; + + %loadCount = %this.loadCount; + + //Process lines + while(!%file.isEOF()) + { + %line = %file.readLine(); + + //Skip empty lines + if(trim(%line $= "")) + continue; + + //Figure out what to do with the line + switch$(getWord(%line, 0)) + { + //Line is brick name + case "+-NTOBJECTNAME": + + $NS[%this, "NT", %index] = getWord(%line, 1); + + //Line is event + case "+-EVENT": + + //Mostly copied from default loading code + %idx = $NS[%this, "EN", %index]; + + if(!%idx) + %idx = 0; + + %enabled = getField(%line, 2); + %inputName = getField(%line, 3); + %delay = getField(%line, 4); + %targetName = getField(%line, 5); + %NT = getField(%line, 6); + %outputName = getField(%line, 7); + %par1 = getField(%line, 8); + %par2 = getField(%line, 9); + %par3 = getField(%line, 10); + %par4 = getField(%line, 11); + + %inputIdx = inputEvent_GetInputEventIdx(%inputName); + + if(%inputIdx == -1) + warn("LOAD DUP: Input Event not found for name \"" @ %inputName @ "\""); + + %targetIdx = inputEvent_GetTargetIndex("FxDTSBrick", %inputIdx, %targetName); + + if(%targetName == -1) + %targetClass = "FxDTSBrick"; + else + { + %field = getField($InputEvent_TargetList["FxDTSBrick", %inputIdx], %targetIdx); + %targetClass = getWord(%field, 1); + } + + %outputIdx = outputEvent_GetOutputEventIdx(%targetClass, %outputName); + + if(%outputIdx == -1) + warn("LOAD DUP: Output Event not found for name \"" @ %outputName @ "\""); + + for(%j = 1; %j < 5; %j++) + { + %field = getField($OutputEvent_ParameterList[%targetClass, %outputIdx], %j - 1); + %dataType = getWord(%field, 0); + + if(%dataType $= "Datablock" && %par[%j] !$= "-1") + { + %par[%j] = nameToId(%par[%j]); + + if(!isObject(%par[%j])) + { + warn("LOAD DUP: Datablock not found for event " @ %outputName @ " -> " @ %par[%j]); + %par[%j] = 0; + } + } + } + + //Save event + $NS[%this, "EE", %index, %idx] = %enabled; + $NS[%this, "ED", %index, %idx] = %delay; + + $NS[%this, "EI", %index, %idx] = %inputName; + $NS[%this, "EII", %index, %idx] = %inputIdx; + + $NS[%this, "EO", %index, %idx] = %outputName; + $NS[%this, "EOI", %index, %idx] = %outputIdx; + $NS[%this, "EOC", %index, %idx] = $OutputEvent_AppendClient["FxDTSBrick", %outputIdx]; + + $NS[%this, "ET", %index, %idx] = %targetName; + $NS[%this, "ETI", %index, %idx] = %targetIdx; + $NS[%this, "ENT", %index, %idx] = %NT; + + $NS[%this, "EP", %index, %idx, 0] = %par1; + $NS[%this, "EP", %index, %idx, 1] = %par2; + $NS[%this, "EP", %index, %idx, 2] = %par3; + $NS[%this, "EP", %index, %idx, 3] = %par4; + + $NS[%this, "EN", %index] = %idx + 1; + + //Line is emitter + case "+-EMITTER": + + %line = getSubStr(%line, 10, 9999); + + %pos = strStr(%line, "\""); + %dbName = getSubStr(%line, 0, %pos); + + if(%dbName !$= "NONE") + { + %db = $UINameTable_Emitters[%dbName]; + + //Ensure emitter exists + if(!isObject(%db)) + { + warn("LOAD DUP: Emitter datablock no found for uiName \"" @ %dbName @ "\""); + %db = 0; + } + } + else + %db = 0; + + $NS[%this, "ED", %index] = %db; + $NS[%this, "ER", %index] = mFLoor(getSubStr(%line, %pos + 2, 9999)); + + //Line is light + case "+-LIGHT": + + %line = getSubStr(%line, 8, 9999); + + %pos = strStr(%line, "\""); + %dbName = getSubStr(%line, 0, %pos); + + %db = $UINameTable_Lights[%dbName]; + + //Ensure light exists + if(!isObject(%db)) + { + warn("LOAD DUP: Light datablock no found for uiName \"" @ %dbName @ "\""); + %db = 0; + } + else + $NS[%this, "LD", %index] = %db; + + //Line is item + case "+-ITEM": + + %line = getSubStr(%line, 7, 9999); + + %pos = strStr(%line, "\""); + %dbName = getSubStr(%line, 0, %pos); + + if(%dbName !$= "NONE") + { + %db = $UINameTable_Items[%dbName]; + + //Ensure item exists + if(!isObject(%db)) + { + warn("LOAD DUP: Item datablock no found for uiName \"" @ %dbName @ "\""); + %db = 0; + } + } + else + %db = 0; + + %line = getSubStr(%line, %pos + 2, 9999); + + $NS[%this, "ID", %index] = %db; + $NS[%this, "IP", %index] = getWord(%line, 0); + $NS[%this, "IR", %index] = getWord(%line, 1); + $NS[%this, "IT", %index] = getWord(%line, 2); + + //Line is music + case "+-AUDIOEMITTER": + + %line = getSubStr(%line, 15, 9999); + + %pos = strStr(%line, "\""); + %dbName = getSubStr(%line, 0, %pos); + + %db = $UINameTable_Music[%dbName]; + + //Ensure music exists + if(!isObject(%db)) + { + warn("LOAD DUP: Music datablock no found for uiName \"" @ %dbName @ "\""); + %db = 0; + } + else + $NS[%this, "MD", %index] = %db; + + //Line is vehicle + case "+-VEHICLE": + + %line = getSubStr(%line, 10, 9999); + + %pos = strStr(%line, "\""); + %dbName = getSubStr(%line, 0, %pos); + + if(%dbName !$= "NONE") + { + %db = $UINameTable_Vehicle[%dbName]; + + //Ensure vehicle exists + if(!isObject(%db)) + { + warn("LOAD DUP: Vehicle datablock no found for uiName \"" @ %dbName @ "\""); + %db = 0; + } + } + else + %db = 0; + + $NS[%this, "VD", %index] = %db; + $NS[%this, "VC", %index] = mFLoor(getSubStr(%line, %pos + 2, 9999)); + + //Start reading connections + case "ND_SIZE\"": + + %version = getWord(%line, 1); + %this.loadExpectedConnectionCount = getWord(%line, 2); + %numberSize = getWord(%line, 3); + %indexSize = getWord(%line, 4); + %connections = true; + break; + + //Error + case "ND_TREE\"": + + warn("LOAD DUP: Got connection data before connection sizes"); + + //Line is irrelevant + case "+-OWNER": + + %nothing = ""; + + //Line is brick + default: + + //Increment selection index + %index++; + %quotePos = strstr(%line, "\""); + + if(%quotePos >= 0) + { + //Get datablock + %uiName = getSubStr(%line, 0, %quotePos); + %db = $uiNameTable[%uiName]; + + if(isObject(%db)) + { + $NS[%this, "D", %index] = %db; + + //Load all the info from brick line + %line = getSubStr(%line, %quotePos + 2, 9999); + %pos = getWords(%line, 0, 2); + %angId = getWord(%line, 3); + + if(%loadCount == 0) + %this.rootPosition = %pos; + + $NS[%this, "P", %index] = vectorSub(%pos, %this.rootPosition); + $NS[%this, "R", %index] = %angId; + + $NS[%this, "CO", %index] = $NS[%this, "CT", getWord(%line, 5)]; + $NS[%this, "CF", %index] = getWord(%line, 7); + $NS[%this, "SF", %index] = getWord(%line, 8); + + if(%db.hasPrint) + { + if((%print = $printNameTable[getWord(%line, 6)]) $= "") + warn("LOAD DUP: Print texture not found for path \"" @ getWord(%line, 6) @ "\""); + + $NS[%this, "PR", %index] = %print; + } + + if(!getWord(%line, 9)) + $NS[%this, "NRC", %index] = true; + + if(!getWord(%line, 10)) + $NS[%this, "NC", %index] = true; + + if(!getWord(%line, 11)) + $NS[%this, "NR", %index] = true; + + //Update selection size with brick datablock + if(%angId % 2 == 0) + { + %sx = %db.brickSizeX / 4; + %sy = %db.brickSizeY / 4; + } + else + { + %sy = %db.brickSizeX / 4; + %sx = %db.brickSizeY / 4; + } + + %sz = %db.brickSizeZ / 10; + + %minX = getWord(%pos, 0) - %sx; + %minY = getWord(%pos, 1) - %sy; + %minZ = getWord(%pos, 2) - %sz; + %maxX = getWord(%pos, 0) + %sx; + %maxY = getWord(%pos, 1) + %sy; + %maxZ = getWord(%pos, 2) + %sz; + + if(%loadCount) + { + if(%minX < %this.minX) + %this.minX = %minX; + + if(%minY < %this.minY) + %this.minY = %minY; + + if(%minZ < %this.minZ) + %this.minZ = %minZ; + + if(%maxX > %this.maxX) + %this.maxX = %maxX; + + if(%maxY > %this.maxY) + %this.maxY = %maxY; + + if(%maxZ > %this.maxZ) + %this.maxZ = %maxZ; + } + else + { + %this.minX = %minX; + %this.minY = %minY; + %this.minZ = %minZ; + %this.maxX = %maxX; + %this.maxY = %maxY; + %this.maxZ = %maxZ; + } + + %loadCount++; + } + else + { + warn("LOAD DUP: Brick datablock not found for uiName \"" @ %uiName @ "\""); + $NS[%this, "D", %index] = 0; + } + } + else + { + warn("LOAD DUP: Brick uiName missing on line \"" @ %line @ "\""); + $NS[%this, "D", %index] = 0; + } + } + + if(%linesProcessed++ > $Pref::Server::ND::ProcessPerTick * 2) + break; + } + + //Save how far we got + %this.loadIndex = %index; + %this.brickCount = %index + 1; + %this.loadCount = %loadCount; + + //Tell the client how much we loaded this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + //Switch over to connection mode if necessary + if(%connections) + { + %this.loadStage = 1; + %this.loadIndex = 0; + %this.connectionCount = 0; + %this.connectionIndex = -1; + %this.connectionIndex2 = 0; + %this.connectionsRemaining = 0; + + if((%numberSize != 1 && %numberSize != 2 && %numberSize != 3) || + (%indexSize != 1 && %indexSize != 2 && %indexSize != 3)) + { + messageClient(%this.client, '', "\c0Warning:\c6 The connection data is corrupted. Planting may not work as expected."); + %this.finishLoading(); + return; + } + + //Create byte table + if(!$ND::Byte241TableCreated) + ndCreateByte241Table(); + + %this.loadSchedule = %this.schedule(30, tickLoadConnections, %numberSize, %indexSize); + return; + } + + //Reached end of file, means we got no connection data + if(%file.isEOF()) + { + messageClient(%this.client, '', "\c0Warning:\c6 The save was not written by the New Duplicator. Planting may not work as expected."); + %this.finishLoading(); + } + else + %this.loadSchedule = %this.schedule(30, tickLoadBricks); +} + +//Load connections +function ND_Selection::tickLoadConnections(%this, %numberSize, %indexSize) +{ + cancel(%this.loadSchedule); + + %connections = %this.connectionCount; + %maxConnections = %this.maxConnections; + %connectionIndex = %this.connectionIndex; + %connectionIndex2 = %this.connectionIndex2; + %connectionsRemaining = %this.connectionsRemaining; + + //Process 10 lines + for(%i = 0; %i < 10 && !%this.loadFile.isEOF(); %i++) + { + %line = getSubStr(%this.loadFile.readLine(), 9, 9999); + %len = strLen(%line); + %pos = 0; + + while(%pos < %len) + { + if(%connectionsRemaining) + { + //Read a connection + if(%indexSize == 1) + { + $NS[%this, "C", %connectionIndex, %connectionIndex2] = + strStr($ND::Byte241Lookup, getSubStr(%line, %pos, 1)); + + %pos++; + } + else if(%indexSize == 2) + { + %tmp = getSubStr(%line, %pos, 2); + + $NS[%this, "C", %connectionIndex, %connectionIndex2] = + strStr($ND::Byte241Lookup, getSubStr(%tmp, 0, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%tmp, 1, 1)); + + %pos += 2; + } + else + { + %tmp = getSubStr(%line, %pos, 3); + + $NS[%this, "C", %connectionIndex, %connectionIndex2] = + ((strStr($ND::Byte241Lookup, getSubStr(%tmp, 0, 1)) * 58081) | 0) + + strStr($ND::Byte241Lookup, getSubStr(%tmp, 1, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%tmp, 2, 1)); + + %pos += 3; + } + + %connectionsRemaining--; + %connectionIndex2++; + %connections++; + } + else + { + //No connections remaining for active brick, increment index + %connectionIndex++; + %connectionIndex2 = 0; + + //Read a connection number + if(%numberSize == 1) + { + %connectionsRemaining = + strStr($ND::Byte241Lookup, getSubStr(%line, %pos, 1)); + + %pos++; + } + else if(%numberSize == 2) + { + %tmp = getSubStr(%line, %pos, 2); + + %connectionsRemaining = + strStr($ND::Byte241Lookup, getSubStr(%tmp, 0, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%tmp, 1, 1)); + + %pos += 2; + } + else + { + %tmp = getSubStr(%line, %pos, 3); + + %connectionsRemaining = + ((strStr($ND::Byte241Lookup, getSubStr(%tmp, 0, 1)) * 58081) | 0) + + strStr($ND::Byte241Lookup, getSubStr(%tmp, 1, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%tmp, 2, 1)); + + %pos += 3; + } + + $NS[%this, "N", %connectionIndex] = %connectionsRemaining; + + if(%maxConnections < %connectionsRemaining) + %maxConnections = %connectionsRemaining; + } + } + } + + //Save how far we got + %this.connectionCount = %connections; + %this.maxConnections = %maxConnections; + %this.connectionIndex = %connectionIndex; + %this.connectionIndex2 = %connectionIndex2; + %this.connectionsRemaining = %connectionsRemaining; + + //Tell the client how much we loaded this tick + if(%this.client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %this.client.ndUpdateBottomPrint(); + %this.client.ndLastMessageTime = $Sim::Time; + } + + //Check if we're done + if(%this.loadFile.isEOF()) + %this.finishLoading(); + else + %this.loadSchedule = %this.schedule(30, tickLoadConnections, %numberSize, %indexSize); +} + +//Finish loading +function ND_Selection::finishLoading(%this) +{ + %this.loadFile.close(); + %this.loadFile.delete(); + + //Align the build to the brick grid + %this.updateSize(); + + %pos = vectorAdd(%this.rootPosition, %this.rootToCenter); + + %shiftX = mCeil(getWord(%pos, 0) * 2 - %this.brickSizeX % 2) / 2 + (%this.brickSizeX % 2) / 4 - getWord(%pos, 0); + %shiftY = mCeil(getWord(%pos, 1) * 2 - %this.brickSizeY % 2) / 2 + (%this.brickSizeY % 2) / 4 - getWord(%pos, 1); + %shiftZ = mCeil(getWord(%pos, 2) * 5 - %this.brickSizeZ % 2) / 5 + (%this.brickSizeZ % 2) / 10 - getWord(%pos, 2); + + %this.rootPosition = vectorAdd(%shiftX SPC %shiftY SPC %shiftZ, %this.rootPosition); + + %this.minX = %this.minX + %shiftX; + %this.maxX = %this.maxX + %shiftX; + %this.minY = %this.minY + %shiftY; + %this.maxY = %this.maxY + %shiftY; + %this.minZ = %this.minZ + %shiftZ; + %this.maxZ = %this.maxZ + %shiftZ; + + %this.updateSize(); + %this.updateHighlightBox(); + + //Message client + %s1 = %this.brickCount == 1 ? "" : "s"; + %s2 = %this.connectionCount == 1 ? "" : "s"; + + messageClient(%this.client, 'MsgProcessComplete', "\c6Finished loading selection, got \c3" + @ %this.brickCount @ "\c6 Brick" @ %s1 @ " with \c3" @ %this.connectionCount @ "\c6 Connection" @ %s2 @ "!"); + + %this.client.ndLastLoadTime = $Sim::Time; + %this.client.ndSetMode(NDM_PlantCopy); +} + +//Cancel loading +function ND_Selection::cancelLoading(%this) +{ + cancel(%this.loadSchedule); + + %this.loadFile.close(); + %this.loadFile.delete(); + + %this.client.ndLastLoadTime = $Sim::Time; +} diff --git a/classes/server/selectionbox.cs b/classes/server/selectionbox.cs new file mode 100644 index 0000000..9387bec --- /dev/null +++ b/classes/server/selectionbox.cs @@ -0,0 +1,496 @@ +// Resizeable box to select all bricks inside a volume. +// ------------------------------------------------------------------- + +//Create a new selection box +function ND_SelectionBox(%shapeName) +{ + ND_ServerGroup.add( + %this = new ScriptObject(ND_SelectionBox) + ); + + %this.innerBox = new StaticShape(){datablock = ND_SelectionBoxInner;}; + %this.outerBox = new StaticShape(){datablock = ND_SelectionBoxOuter;}; + %this.shapeName = new StaticShape(){datablock = ND_SelectionBoxShapeName;}; + + %this.corner1 = new StaticShape(){datablock = ND_SelectionBoxOuter;}; + %this.corner2 = new StaticShape(){datablock = ND_SelectionBoxOuter;}; + %this.selectedCorner = true; + + %this.innerBox.setScopeAlways(); + %this.outerBox.setScopeAlways(); + %this.shapeName.setScopeAlways(); + %this.corner1.setScopeAlways(); + %this.corner2.setScopeAlways(); + + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i] = new StaticShape(){datablock = ND_SelectionBoxBorder;}; + %this.border_y[%i] = new StaticShape(){datablock = ND_SelectionBoxBorder;}; + %this.border_z[%i] = new StaticShape(){datablock = ND_SelectionBoxBorder;}; + + %this.border_x[%i].setScopeAlways(); + %this.border_y[%i].setScopeAlways(); + %this.border_z[%i].setScopeAlways(); + } + + %this.boxName = %shapeName; + %this.setNormalMode(); + + return %this; +} + +//Destroy static shapes when selection box is removed +function ND_SelectionBox::onRemove(%this) +{ + %this.innerBox.delete(); + %this.outerBox.delete(); + %this.shapeName.delete(); + + %this.corner1.delete(); + %this.corner2.delete(); + + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i].delete(); + %this.border_y[%i].delete(); + %this.border_z[%i].delete(); + } +} + +//Set normal color values and borders +function ND_SelectionBox::setNormalMode(%this) +{ + %this.innerColor = "0 0 0 0.60"; + %this.outerColor = "0 0 0 0.35"; + + %this.borderColor = "1 0.84 0 0.99"; + %this.borderColorSelected = "0 0 1 0.99"; + + %this.cornerColor1 = "0.8 0.74 0 0.99"; + %this.cornerColor2 = "1 0.94 0.1 0.99"; + + %this.cornerColorSelected1 = "0 0.2 1 0.99"; + %this.cornerColorSelected2 = "0 0.1 0.9 0.99"; + + %this.isNormalMode = true; + + //Unhide the corners and inner/outer box (hidden in disabled mode) + %this.innerBox.unHideNode("ALL"); + %this.corner1.unHideNode("ALL"); + %this.corner2.unHideNode("ALL"); + + if(%this.hasVolume()) + %this.outerBox.unHideNode("ALL"); + + //Apply changes + %this.applyColors(); + %this.setSize(%this.point1, %this.point2); + %this.shapeName.setShapeName(%this.boxName); +} + +//Set grayscale color values and slightly smaller border +function ND_SelectionBox::setDisabledMode(%this) +{ + %this.borderColor = "0.1 0.1 0.1 0.4"; + %this.borderColorSelected = "0.1 0.1 0.1 0.4"; + + %this.isNormalMode = false; + + //Hide the corners and inner/outer box (looks better) + %this.innerBox.hideNode("ALL"); + %this.outerBox.hideNode("ALL"); + %this.corner1.hideNode("ALL"); + %this.corner2.hideNode("ALL"); + + //Apply changes + %this.applyColors(); + %this.setSize(%this.point1, %this.point2); + %this.shapeName.setShapeName(""); +} + +//Apply color changes to the selection box +function ND_SelectionBox::applyColors(%this) +{ + %this.innerBox.setNodeColor("ALL", %this.innerColor); + %this.outerBox.setNodeColor("ALL", %this.outerColor); + + %this.shapeName.setShapeNameColor(%this.borderColor); + + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i].setNodeColor("ALL", %this.borderColor); + %this.border_y[%i].setNodeColor("ALL", %this.borderColor); + %this.border_z[%i].setNodeColor("ALL", %this.borderColor); + } + + %bColor = %this.borderColorSelected; + + if(%this.selectedCorner) + { + %this.border_x2.setNodeColor("ALL", %bColor); + %this.border_y2.setNodeColor("ALL", %bColor); + %this.border_z2.setNodeColor("ALL", %bColor); + + %corner1 = %this.corner1; + %corner2 = %this.corner2; + } + else + { + %this.border_x0.setNodeColor("ALL", %bColor); + %this.border_y0.setNodeColor("ALL", %bColor); + %this.border_z0.setNodeColor("ALL", %bColor); + + %corner1 = %this.corner2; + %corner2 = %this.corner1; + } + + %corner1.setNodeColor("out+X", %this.borderColor); + %corner1.setNodeColor("out-X", %this.borderColor); + %corner1.setNodeColor("out+Y", %this.cornerColor1); + %corner1.setNodeColor("out-Y", %this.cornerColor1); + %corner1.setNodeColor("out+Z", %this.cornerColor2); + %corner1.setNodeColor("out-Z", %this.cornerColor2); + + //Illusion of shaded box + %corner2.setNodeColor("out+X", %this.borderColorSelected); + %corner2.setNodeColor("out-X", %this.borderColorSelected); + %corner2.setNodeColor("out+Y", %this.cornerColorSelected1); + %corner2.setNodeColor("out-Y", %this.cornerColorSelected1); + %corner2.setNodeColor("out+Z", %this.cornerColorSelected2); + %corner2.setNodeColor("out-Z", %this.cornerColorSelected2); +} + +//Return current size of selection box +function ND_SelectionBox::getSize(%this) +{ + %x1 = getWord(%this.point1, 0); + %y1 = getWord(%this.point1, 1); + %z1 = getWord(%this.point1, 2); + + %x2 = getWord(%this.point2, 0); + %y2 = getWord(%this.point2, 1); + %z2 = getWord(%this.point2, 2); + + %min = getMin(%x1, %x2) SPC getMin(%y1, %y2) SPC getMin(%z1, %z2); + %max = getMax(%x1, %x2) SPC getMax(%y1, %y2) SPC getMax(%z1, %z2); + + return vectorSub(%max, %min); +} + +//Return current world box of selection box +function ND_SelectionBox::getWorldBox(%this) +{ + %x1 = getWord(%this.point1, 0); + %y1 = getWord(%this.point1, 1); + %z1 = getWord(%this.point1, 2); + + %x2 = getWord(%this.point2, 0); + %y2 = getWord(%this.point2, 1); + %z2 = getWord(%this.point2, 2); + + %min = getMin(%x1, %x2) SPC getMin(%y1, %y2) SPC getMin(%z1, %z2); + %max = getMax(%x1, %x2) SPC getMax(%y1, %y2) SPC getMax(%z1, %z2); + + return %min SPC %max; +} + +//Resize the selection box +function ND_SelectionBox::setSize(%this, %point1, %point2) +{ + if(getWordCount(%point1) == 6) + { + %point2 = getWords(%point1, 3, 5); + %point1 = getWords(%point1, 0, 2); + } + + %this.point1 = %point1; + %this.point2 = %point2; + + %x1 = getWord(%point1, 0); + %y1 = getWord(%point1, 1); + %z1 = getWord(%point1, 2); + + %x2 = getWord(%point2, 0); + %y2 = getWord(%point2, 1); + %z2 = getWord(%point2, 2); + + %len_x = mAbs(%x2 - %x1); + %len_y = mAbs(%y2 - %y1); + %len_z = mAbs(%z2 - %z1); + + %center_x = (%x1 + %x2) / 2; + %center_y = (%y1 + %y2) / 2; + %center_z = (%z1 + %z2) / 2; + + %rot_x = "0 1 0 1.57079"; + %rot_y = "1 0 0 1.57079"; + %rot_z = "0 0 1 0"; + + %this.innerBox.setTransform(%center_x SPC %center_y SPC %center_z); + %this.outerBox.setTransform(%center_x SPC %center_y SPC %center_z); + %this.shapeName.setTransform(%center_X SPC %center_y SPC %z2); + + %this.border_x0.setTransform(%center_x SPC %y1 SPC %z1 SPC %rot_x); + %this.border_x1.setTransform(%center_x SPC %y2 SPC %z1 SPC %rot_x); + %this.border_x2.setTransform(%center_x SPC %y2 SPC %z2 SPC %rot_x); + %this.border_x3.setTransform(%center_x SPC %y1 SPC %z2 SPC %rot_x); + + %this.border_y0.setTransform(%x1 SPC %center_y SPC %z1 SPC %rot_y); + %this.border_y1.setTransform(%x2 SPC %center_y SPC %z1 SPC %rot_y); + %this.border_y2.setTransform(%x2 SPC %center_y SPC %z2 SPC %rot_y); + %this.border_y3.setTransform(%x1 SPC %center_y SPC %z2 SPC %rot_y); + + %this.border_z0.setTransform(%x1 SPC %y1 SPC %center_z SPC %rot_z); + %this.border_z1.setTransform(%x2 SPC %y1 SPC %center_z SPC %rot_z); + %this.border_z2.setTransform(%x2 SPC %y2 SPC %center_z SPC %rot_z); + %this.border_z3.setTransform(%x1 SPC %y2 SPC %center_z SPC %rot_z); + + %this.corner1.setTransform(%x1 SPC %y1 SPC %z1); + %this.corner2.setTransform(%x2 SPC %y2 SPC %z2); + + %this.innerBox.setScale(%len_x - 0.02 SPC %len_y - 0.02 SPC %len_z - 0.02); + %this.outerBox.setScale(%len_x + 0.02 SPC %len_y + 0.02 SPC %len_z + 0.02); + + if(%this.isNormalMode) + { + //Normal mode (box with two colored corners) + %maxLen = getMax(getMax(%len_x, %len_y), %len_z); + %width = (7/1024) * %maxLen + 1; + + for(%i = 0; %i < 4; %i++) + { + %this.border_x[%i].setScale(%width SPC %width SPC %len_x + %width * 0.05); + %this.border_y[%i].setScale(%width SPC %width SPC %len_y + %width * 0.05); + %this.border_z[%i].setScale(%width SPC %width SPC %len_z + %width * 0.05); + } + + if(%this.selectedCorner) + { + %width1 = %width; + %width2 = %width + 0.02; + } + else + { + %width1 = %width + 0.02; + %width2 = %width; + } + + //The borders touching the two corners are thicker to prevent Z fighting + //with the highlight box if it covers the same area as the selection + %this.border_x0.setScale(%width1 SPC %width1 SPC %len_x - %width * 0.05); + %this.border_y0.setScale(%width1 SPC %width1 SPC %len_y - %width * 0.05); + %this.border_z0.setScale(%width1 SPC %width1 SPC %len_z - %width * 0.05); + + %this.border_x2.setScale(%width2 SPC %width2 SPC %len_x - %width * 0.05); + %this.border_y2.setScale(%width2 SPC %width2 SPC %len_y - %width * 0.05); + %this.border_z2.setScale(%width2 SPC %width2 SPC %len_z - %width * 0.05); + + //Corners scale with the border width + %cs1 = 0.35 * %width1; + %cs2 = 0.35 * %width2; + + %this.corner1.setScale(%cs1 SPC %cs1 SPC %cs1); + %this.corner2.setScale(%cs2 SPC %cs2 SPC %cs2); + } + else + { + //Disabled mode (transparent greyscale box) + %maxLen = getMax(getMax(%len_x, %len_y), %len_z); + %width = (21/5120) * %maxLen + 1; + + for(%i = 0; %i < 4; %i++) + { + //Horizontal borders are a bit shorter to prevent z fighting + %this.border_x[%i].setScale(%width SPC %width SPC %len_x - %width * 0.05); + %this.border_y[%i].setScale(%width SPC %width SPC %len_y - %width * 0.05); + + %this.border_z[%i].setScale(%width SPC %width SPC %len_z + %width * 0.05); + } + } +} + +//Resize the selection box and align it to a player +function ND_SelectionBox::setSizeAligned(%this, %point1, %point2, %player) +{ + //Set the selection box to correct orientation + %x1 = getWord(%point1, 0); + %y1 = getWord(%point1, 1); + %z1 = getWord(%point1, 2); + + %x2 = getWord(%point2, 0); + %y2 = getWord(%point2, 1); + %z2 = getWord(%point2, 2); + + switch(getAngleIDFromPlayer(%player)) + { + case 0: + %p1 = %x1 SPC %y2 SPC %z1; + %p2 = %x2 SPC %y1 SPC %z2; + + case 1: + %p1 = %x1 SPC %y1 SPC %z1; + %p2 = %x2 SPC %y2 SPC %z2; + + case 2: + %p1 = %x2 SPC %y1 SPC %z1; + %p2 = %x1 SPC %y2 SPC %z2; + + case 3: + %p1 = %x2 SPC %y2 SPC %z1; + %p2 = %x1 SPC %y1 SPC %z2; + } + + //Select first corner + if(!%this.selectedCorner) + { + %this.selectedCorner = true; + %this.applyColors(); + } + + %this.setSize(%p1, %p2); +} + +//Select one of the two corners +function ND_SelectionBox::switchCorner(%this) +{ + %this.selectedCorner = !%this.selectedCorner; + + if(%this.selectedCorner) + serverPlay3d(BrickRotateSound, %this.point2); + else + serverPlay3d(BrickRotateSound, %this.point1); + + %this.setSize(%this.point1, %this.point2); + %this.applyColors(); +} + +//Move the selected corner +function ND_SelectionBox::shiftCorner(%this, %offset, %limit) +{ + %oldP1 = %this.point1; + %oldP2 = %this.point2; + %limitReached = false; + + //Size of a plate in TU + %unit[0] = 0.5; + %unit[1] = 0.5; + %unit[2] = 0.2; + + for(%dim = 0; %dim < 3; %dim++) + { + //Copy current + %point1[%dim] = getWord(%this.point1, %dim); + %point2[%dim] = getWord(%this.point2, %dim); + + //Get the size of the box in the current axis after resizing + %size = getWord(%this.point2, %dim) - getWord(%this.point1, %dim); + + if(%this.selectedCorner) + { + //Update point2 + %size += getWord(%offset, %dim); + %point2[%dim] += getWord(%offset, %dim); + + //Check limits + if(mAbs(%size) > %limit) + { + %limitReached = true; + %point2[%dim] -= %size - %limit * (mAbs(%size) / %size); + } + } + else + { + //Update point1 + %size -= getWord(%offset, %dim); + %point1[%dim] += getWord(%offset, %dim); + + //Check limits + if(mAbs(%size) > %limit) + { + %limitReached = true; + %point1[%dim] += %size - %limit * (mAbs(%size) / %size); + } + } + } + + //Update corner positions + %point1 = %point1[0] SPC %point1[1] SPC %point1[2]; + %point2 = %point2[0] SPC %point2[1] SPC %point2[2]; + %this.setSize(%point1, %point2); + + //Play sounds + if(%this.selectedCorner) + %soundPoint = %point2; + else + %soundPoint = %point1; + + if(%point1 !$= %oldP1 || %point2 !$= %oldP2) + serverPlay3d(BrickMoveSound, %soundPoint); + else + serverPlay3d(errorSound, %soundPoint); + + //Hide outer box on selection boxes without volume + if(%this.hasVolume()) + %this.outerBox.unHideNode("ALL"); + else + %this.outerBox.hideNode("ALL"); + + return %limitReached; +} + +//Move the entire box +function ND_SelectionBox::shift(%this, %offset) +{ + %this.point1 = vectorAdd(%this.point1, %offset); + %this.point2 = vectorAdd(%this.point2, %offset); + + %this.setSize(%this.point1, %this.point2); + + //Play sounds + if(%this.selectedCorner) + serverPlay3d(BrickMoveSound, %this.point1); + else + serverPlay3d(BrickMoveSound, %this.point2); +} + +//Rotate the entire box +function ND_SelectionBox::rotate(%this, %direction) +{ + %point1 = %this.point1; + %point2 = %this.point2; + %center = vectorScale(vectorAdd(%point1, %point2), 0.5); + + %brickSizeX = mAbs(mFloatLength((getWord(%point2, 0) - getWord(%point1, 0)) * 2, 0)); + %brickSizeY = mAbs(mFloatLength((getWord(%point2, 1) - getWord(%point1, 1)) * 2, 0)); + + %shiftCorrect = "0 0 0"; + + if((%brickSizeX % 2) != (%brickSizeY % 2)) + { + if(%brickSizeX % 2) + %shiftCorrect = "-0.25 -0.25 0"; + else + %shiftCorrect = "0.25 0.25 0"; + } + + %point1 = vectorAdd(ndRotateVector(vectorSub(%point1, %center), %direction), %center); + %point2 = vectorAdd(ndRotateVector(vectorSub(%point2, %center), %direction), %center); + %this.setSize(vectorAdd(%point1, %shiftCorrect), vectorAdd(%point2, %shiftCorrect)); + + //Play sounds + if(%this.selectedCorner) + serverPlay3d(BrickRotateSound, %this.point1); + else + serverPlay3d(BrickRotateSound, %this.point2); +} + +//Check if the box has a volume +function ND_SelectionBox::hasVolume(%this) +{ + if(mAbs(getWord(%this.point1, 0) - getWord(%this.point2, 0)) < 0.05 + || mAbs(getWord(%this.point1, 1) - getWord(%this.point2, 1)) < 0.05 + || mAbs(getWord(%this.point1, 2) - getWord(%this.point2, 2)) < 0.05) + return false; + + return true; +} diff --git a/classes/server/undogrouppaint.cs b/classes/server/undogrouppaint.cs new file mode 100644 index 0000000..bf6d8a1 --- /dev/null +++ b/classes/server/undogrouppaint.cs @@ -0,0 +1,75 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Delete this undo group +function ND_UndoGroupPaint::onRemove(%this) +{ + if(%this.brickCount) + deleteVariables("$NU" @ %this.client @ "_" @ %this @ "_*"); +} + +//Start undo paint +function ND_UndoGroupPaint::ndStartUndo(%this, %client) +{ + %client.ndUndoInProgress = true; + %client.ndLastMessageTime = $Sim::Time; + %this.ndTickUndo(%this.paintType, 0, %client); +} + +//Tick undo paint +function ND_UndoGroupPaint::ndTickUndo(%this, %mode, %start, %client) +{ + %end = %start + $Pref::Server::ND::ProcessPerTick; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + for(%i = %start; %i < %end; %i++) + { + %brick = $NU[%client, %this, "B", %i]; + + if(!isObject(%brick)) + continue; + + %colorID = $NU[%client, %this, "V", %i]; + + switch(%mode) + { + case 0: + //Check whether brick is highlighted + %brick.setColor(%colorID); + + case 1: + //Check whether brick is highlighted + if($NDHN[%brick]) + $NDHF[%brick] = %colorID; + else + %brick.setColorFx(%colorID); + + case 2: + %brick.setShapeFx(%colorID); + } + } + + //If undo is taking long, tell the client how far we get + if(%client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %client.ndLastMessageTime = $Sim::Time; + + %percent = mFloor(%end * 100 / %this.brickCount); + commandToClient(%client, 'centerPrint', "\c6Undo in progress...\n\c3" @ %percent @ "%\c6 finished.", 10); + } + + if(%end >= %this.brickcount) + { + %this.delete(); + %client.ndUndoInProgress = false; + + if(%start != 0) + commandToClient(%client, 'centerPrint', "\c6Undo finished.", 2); + + return; + } + + %this.schedule(30, ndTickUndo, %mode, %end, %client); +} diff --git a/classes/server/undogroupplant.cs b/classes/server/undogroupplant.cs new file mode 100644 index 0000000..2c2bb44 --- /dev/null +++ b/classes/server/undogroupplant.cs @@ -0,0 +1,61 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Start undo bricks +function SimSet::ndStartUndo(%this, %client) +{ + if(!%this.brickCount) + { + %this.delete(); + return; + } + + %client.ndUndoInProgress = true; + %client.ndLastMessageTime = $Sim::Time; + %this.ndTickUndo(%this.brickCount, %client); +} + +//Tick undo bricks +function SimSet::ndTickUndo(%this, %count, %client) +{ + if(%count > %this.getCount()) + %start = %this.getCount(); + else + %start = %count; + + if(%start > $Pref::Server::ND::ProcessPerTick) + %end = %start - $Pref::Server::ND::ProcessPerTick; + else + %end = 0; + + for(%i = %start - 1; %i >= %end; %i--) + { + %brick = %this.getObject(%i); + %brick.killBrick(); + + if(%start > 1024) + %brick.delete(); + } + + //If undo is taking long, tell the client how far we get + if(%client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %client.ndLastMessageTime = $Sim::Time; + + %percent = mFloor(100 - (%end * 100 / %this.brickCount)); + commandToClient(%client, 'centerPrint', "\c6Undo in progress...\n\c3" @ %percent @ "%\c6 finished.", 10); + } + + if(%end <= 0) + { + %this.delete(); + %client.ndUndoInProgress = false; + + if(%start != 0) + commandToClient(%client, 'centerPrint', "\c6Undo finished.", 2); + + return; + } + + %this.schedule(30, ndTickUndo, %end, %client); +} diff --git a/classes/server/undogroupwrench.cs b/classes/server/undogroupwrench.cs new file mode 100644 index 0000000..29da4eb --- /dev/null +++ b/classes/server/undogroupwrench.cs @@ -0,0 +1,197 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Delete this undo group +function ND_UndoGroupWrench::onRemove(%this) +{ + if(%this.brickCount) + deleteVariables("$NU" @ %this.client @ "_" @ %this @ "_*"); +} + +//Start undo wrench +function ND_UndoGroupWrench::ndStartUndo(%this, %client) +{ + %client.ndUndoInProgress = true; + %client.ndLastMessageTime = $Sim::Time; + %this.ndTickUndo(0, %client); +} + +//Tick undo wrench +function ND_UndoGroupWrench::ndTickUndo(%this, %start, %client) +{ + %end = %start + $Pref::Server::ND::ProcessPerTick; + + if(%end > %this.brickCount) + %end = %this.brickCount; + + setCurrentQuotaObject(getQuotaObjectFromClient(%client)); + + %fillWrenchName = %this.fillWrenchName; + %fillWrenchLight = %this.fillWrenchLight; + %fillWrenchEmitter = %this.fillWrenchEmitter; + %fillWrenchEmitterDir = %this.fillWrenchEmitterDir; + %fillWrenchItem = %this.fillWrenchItem; + %fillWrenchItemPos = %this.fillWrenchItemPos; + %fillWrenchItemDir = %this.fillWrenchItemDir; + %fillWrenchItemTime = %this.fillWrenchItemTime; + %fillWrenchRaycasting = %this.fillWrenchRaycasting; + %fillWrenchCollision = %this.fillWrenchCollision; + %fillWrenchRendering = %this.fillWrenchRendering; + + for(%i = %start; %i < %end; %i++) + { + %brick = $NU[%client, %this, "B", %i]; + + if(!isObject(%brick)) + continue; + + //Revert wrench settings + if(%fillWrenchName) + { + %curr = getSubStr(%brick.getName(), 1, 254); + %fillWrenchNameValue = $NU[%client, %this, "N", %i]; + + if(%curr !$= %fillWrenchNameValue) + %brick.setNTObjectName(%fillWrenchNameValue); + } + + if(%fillWrenchLight) + { + if(%tmp = %brick.light | 0) + %curr = %tmp.getDatablock(); + else + %curr = 0; + + %fillWrenchLightValue = $NU[%client, %this, "LDB", %i]; + + if(%curr != %fillWrenchLightValue) + %brick.setLight(%fillWrenchLightValue); + } + + if(%fillWrenchEmitter) + { + if(%tmp = %brick.emitter | 0) + %curr = %tmp.getEmitterDatablock(); + else if(%tmp = %brick.oldEmitterDB | 0) + %curr = %tmp; + else + %curr = 0; + + %fillWrenchEmitterValue = $NU[%client, %this, "EDB", %i]; + + if(%curr != %fillWrenchEmitterValue) + %brick.setEmitter(%fillWrenchEmitterValue); + } + + if(%fillWrenchEmitterDir) + { + %curr = %brick.emitterDirection; + %fillWrenchEmitterDirValue = $NU[%client, %this, "EDIR", %i]; + + if(%curr != %fillWrenchEmitterDirValue) + %brick.setEmitterDirection(%fillWrenchEmitterDirValue); + } + + if(%fillWrenchItem) + { + if(%tmp = %brick.item | 0) + %curr = %tmp.getDatablock(); + else + %curr = 0; + + %fillWrenchItemValue = $NU[%client, %this, "IDB", %i]; + + if(%curr != %fillWrenchItemValue) + %brick.setItem(%fillWrenchItemValue); + } + + if(%fillWrenchItemPos) + { + %curr = %brick.itemPosition; + %fillWrenchItemPosValue = $NU[%client, %this, "IPOS", %i]; + + if(%curr != %fillWrenchItemPosValue) + %brick.setItemPosition(%fillWrenchItemPosValue); + } + + if(%fillWrenchItemDir) + { + %curr = %brick.itemPosition; + %fillWrenchItemDirValue = $NU[%client, %this, "IDIR", %i]; + + if(%curr != %fillWrenchItemDirValue) + %brick.setItemDirection(%fillWrenchItemDirValue); + } + + if(%fillWrenchItemTime) + { + %curr = %brick.itemRespawnTime; + %fillWrenchItemTimeValue = $NU[%client, %this, "IRT", %i]; + + if(%curr != %fillWrenchItemTimeValue) + %brick.setItemRespawnTime(%fillWrenchItemTimeValue); + } + + if(%fillWrenchRaycasting) + { + %curr = %brick.isRaycasting(); + %fillWrenchRaycastingValue = $NU[%client, %this, "RC", %i]; + + if(%curr != %fillWrenchRaycastingValue) + %brick.setRaycasting(%fillWrenchRaycastingValue); + } + + if(%fillWrenchCollision) + { + %curr = %brick.isColliding(); + %fillWrenchCollisionValue = $NU[%client, %this, "C", %i]; + + if(%curr != %fillWrenchCollisionValue) + %brick.setColliding(%fillWrenchCollisionValue); + } + + if(%fillWrenchRendering) + { + %curr = %brick.isRendering(); + %fillWrenchRenderingValue = $NU[%client, %this, "R", %i]; + + if(%curr != %fillWrenchRenderingValue) + { + //Copy emitter ...? + if(!%fillWrenchRenderingValue && (%tmp = %brick.emitter | 0)) + %emitter = %tmp.getEmitterDatablock(); + else + %emitter = 0; + + %brick.setRendering(%fillWrenchRenderingValue); + + if(!%fillWrenchRenderingValue && %emitter) + %brick.setEmitter(%emitter); + } + } + } + + clearCurrentQuotaObject(); + + //If undo is taking long, tell the client how far we get + if(%client.ndLastMessageTime + 0.1 < $Sim::Time) + { + %client.ndLastMessageTime = $Sim::Time; + + %percent = mFloor(%end * 100 / %this.brickCount); + commandToClient(%client, 'centerPrint', "\c6Undo in progress...\n\c3" @ %percent @ "%\c6 finished.", 10); + } + + if(%end >= %this.brickcount) + { + %this.delete(); + %client.ndUndoInProgress = false; + + if(%start != 0) + commandToClient(%client, 'centerPrint', "\c6Undo finished.", 2); + + return; + } + + %this.schedule(30, ndTickUndo, %end, %client); +} diff --git a/description.txt b/description.txt new file mode 100644 index 0000000..4babe57 --- /dev/null +++ b/description.txt @@ -0,0 +1,4 @@ +Title: New Duplicator +Author: Zeblote (1163) +New lag-free duplicator with intelligent selection modes +Redo's mod v1: better dupsave listing and search, fill and supercut logic wires, V20 support, possibly some other stuff I forgot about diff --git a/resources/server/black.png b/resources/server/black.png new file mode 100644 index 0000000000000000000000000000000000000000..6f455b6cf27eec95590ec1b270e537b949a55b8f GIT binary patch literal 82 zcmeAS@N?&q;$mQ6;Pv!y2?EjrAk4@NBvoy5m4Fm;fKQ04q@<+xvn79lTy9Sn$B>G+ bWCcMW$AE#c(PNnlkj3EX>gTe~DWM4fDo78g literal 0 HcmV?d00001 diff --git a/resources/server/blank.png b/resources/server/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..02156f9b267003a3b0eacd994a407a0950002ce8 GIT binary patch literal 84 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

T9X{UEqxy7v>aGss+R< zK2_(9WliXCqJq!mxU(mrg__903i#5v#UlWJFdLWff@4Q%qEkbKRD00M#A_3|F@1n~ z&659WwaV=H&^*&l7v4R;dg3TBycXiE z_MHV=&0|cOJ*g8NGd*~vY%=X}2+#-JGyBgs+&(G>voO&qcjjI$*1^U^=LQuJ<`jed zj%oWg3ar|=DgZ_WNk7w$%YMyF+HG96o*XX;1NvGOMr2Odg>U=UVMp^pp|{7!LG2>c z@JBkP`%)*V<%60rev<~NRFiqHcDutilX|)a>4SM^?-YCS+Il1YMuW|Ucau}36&C4N zo7&l}nQ5uL<{C%VK&xCCe)I?Wctauj$szan_@cB{G3C~W9e1mn@y zMSI3pw_ooD>I8ofU2`H_+fkSL!9G%PHuVSSY*|stGf5L+RaWVLn)wafe0MGJ%mTus zk$*&I-Oy3#)=o!W%Wib3Ph~`eYYXFkK8Sg2EO2ron2m-dYyYU5_bWKh6y}{=cAPDV z3#T+xOOy2Qc%ON>sfi$Z)FVXbSG6!O-8=!qApj#=N6OF-oMc|DULHe`77s5M*~*`@ z7AZMOt2<^We`88~mVOt`Z;neTZ}OtPG}4aDs>i>?Nl?el!mWR@;ko90(8atM_Uy{RBj(vVm~ib62~#3%Kk;=>&>-syF}>!SoLF{ z`Vih2D$RumSt`DCof_O*Z9NYZnJa3?HVnHk%0A^az`!vHn&-VEgVP}|Mo4&_ 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 RDB1u3002ovPDHLkV1n6NeAfT~ literal 0 HcmV?d00001 diff --git a/resources/server/brickSIDE.png b/resources/server/brickSIDE.png new file mode 100644 index 0000000000000000000000000000000000000000..b821ad9574d5cf288dd13fefcaf86acbbd501b69 GIT binary patch literal 10058 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 H-^Bj|viN6_ literal 0 HcmV?d00001 diff --git a/resources/server/brickTOP.png b/resources/server/brickTOP.png new file mode 100644 index 0000000000000000000000000000000000000000..db2f93de6dda74631cc7226c9a717979ddebd797 GIT binary patch literal 44652 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 literal 0 HcmV?d00001 diff --git a/resources/server/duplicator_brick.dts b/resources/server/duplicator_brick.dts new file mode 100644 index 0000000000000000000000000000000000000000..4da67e32274ee2c3c7c069fc523092be5eef98ad GIT binary patch literal 14772 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 literal 0 HcmV?d00001 diff --git a/resources/server/icon.png b/resources/server/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..831d7e0100b70660700dd2b1efd60709d79fd281 GIT binary patch literal 3019 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 N002ovPDHLkV1fW;yOsa| literal 0 HcmV?d00001 diff --git a/resources/server/selectionbox_border.dts b/resources/server/selectionbox_border.dts new file mode 100644 index 0000000000000000000000000000000000000000..aca9e6a158e2c44dcafcf9f707e739fb16467b92 GIT binary patch literal 3785 zcmchZO;1xn6oyX=luxlDA|loTCay5xx1pvRe}YR~U~G(up`@~K2WvN7_!IaUZPLWV z9Sc_c6L$tY&vZ`j+{;`kiE)y-^WJyP%$#@T%*UdL9F2%nG&eO*>;IHyQlVeue$CEo z`Jd4ow)#&pdk6&GKQKYA3jjXtC?anhP#;d(cp{crmfg6o*V-cvvc;JCXl~nHToaHO=58;lrW&e%PB=a@JQ|*OIv>Gw;7okG3>-G-llX z^0@wRyIlj*tUe`mVkEQ;u$vQW$NYHQqs>t1>>xgO5gxxaRf!3bjZyCZ$_*_u`a0^9 zJ3sKt{G?QkE;a&vR^Q9o`abIL)}M{-hmIE!4bCu!&(k;Od%vk=s1IM%r!Cgc`wV9N z98a^1z6-WNAFSn|zoGOoz}bPunI&S*rwE^?j~FH0(j4kTo9R>Eh%M9SFl_>T1KXgT z#iwf*^f7#Vx_!h*nj)G)K2P6*GZMM2Wkw${H&_$=WBrsPKHXUq=u6q!Twoiv^LR*o z!C93Pld5^JKlAi0dVO~;>PxLj;OACEU&=Pnw-vU#lD>$ zFZkc_>xaJV`C|3#Rl}?``sgFX!>-?7_nUJ&n1eih%U<8TjJ}iO<1YEz=~_)om%-j# zz`WTwD>AmUs|D>Y^fC1L;s#6BsDH1S2WdV}U&Wi>*Ag2t_g~<%^Vj?A){Q3We>b8J zThnLl&}SIk-7D%F>@n4si}UnVaaPlc=1?DTv(MUL-u~R`jll35 zSAm}3EQ#@X`c}1MXiBEEGpZ$4cW9lT*_(*JcqXWgbs5hX9%9B9;ZND_O)X zS;7%nW@xD}m{eEzuC6t{?loKcjn;ar+1!`+&DMuT>(R!>s|3DG;EM!4PvElzK26}0 s1U^pSrib#TvA^@_^ZKR_3MnuV({EclyI=NpT8-WPkNVFhw`2kL2gKlq)Bpeg literal 0 HcmV?d00001 diff --git a/resources/server/selectionbox_inner.dts b/resources/server/selectionbox_inner.dts new file mode 100644 index 0000000000000000000000000000000000000000..848308cca6e9a6aa2a5c72c4bfa88f068e20d47d GIT binary patch literal 2405 zcmbW2KW|e}48@%^P1~d`C6o|S2KFjKNM+8##LNKQIz*@vYO9ceE%{L0^Pvz6L(BO& z*T{K#sUj@-`P_4TeeL`24b1FQ)9g>f?49hM+HEN!>PZPdobaW+U$BhJ@79O$FKM)k zUCJ&s9hSba@HpmstVkU%f0yjKQ^U1txZN5qYRU1KgSlgVZ1m?oaPZ`S9~?M%@_>f} z2Tv~WaNyv{2ObU_e2kA8)a8Ot-aBxO#6Ga@S*mT{lk8CNlKnl9cAQv5JhlEGcQyJ@ zdL&)Xo0tpbwph}CEJdD4SsRpfk&%zDId)vITQI^oEN3D62FBO@wQq2NV+k-em$jxKK+!3eNKFnjh zx)=30`E07MSAxHgt<>i^Ro6b}lIqL8fyoKw-oXXle*bb^sTRw)_-v~0lmsL%r8V`D z*M*eaP;kfvZ)7^3;foDlZ17;`x7xp$#qUxy(z&ego9gSAp|4~s_0f~hoS&`Y?k4SD z)t7P9f-sI65s$H5eW{jw4>L|RZmMsvn%7>-R_Y@s^j=D?C^+Q4X8-7CO!#7Huf^D| zK8Fw>zu5Zr2>NEPn2XubP r$D5b-aWcEQ{yutN7SI}`+WkDey7_T;HJ#kdzNsBQ>eY{FwMl;gK(8^A literal 0 HcmV?d00001 diff --git a/resources/server/selectionbox_outer.dts b/resources/server/selectionbox_outer.dts new file mode 100644 index 0000000000000000000000000000000000000000..f2dfaa63eef44d31292a878824a9aab7b3804920 GIT binary patch literal 2409 zcmbW2&rVZO5XPspl+prXL9E3Ed(x<(ZfO_p+`3S9EE;2CD5*7ZM<2mQ;1Nvdn(|Ht zH6&8M-}zHc|E6)0bI;uG%*>hZ%-nM;X7+Z)>@H*WLi3v5^BRbtg%a&B!j|_!&^0;v zlY8CxsiBu6v)&b8)@-nW$z?4Rh%hI@ux8DJ%vRsX*~8| z@3=p6Tu;}~BTf$3fuTp7Jix)wBTg>hVCWGiA8;`Ah)4g(K}$}sNo$8bA%lr`Y7%AE zTDm^6qrU4JMThhfoB!8KinXD!sWF~6F?-6bH(-5B19_yu*)TX4vu6MAaCogVmu<7I zGjjc;WyyA(=dF6s_d@S#hAn>mJ0GJVbM>l^;eCj(NXRitqTgRU;E7J&l7rZDagS|} z+hffsKT*6k``(A2Y?WWLE?qO+n(mwWHR1U~V0P*FX-TmozZ-G$cg6%igP&r^@8$^ob~ro^F}P2J`xxZfyZ@iZ+f)3x%RccU6wKh}?R_=RmLzCUH1+#%eFLV}Fz^Y~R|y|3}yd1dqsWi6*aOn!9h4ljJbOmEq_pua)UO1i%{ zbw6`6=XG{j#U{RPD0iFXg^-n43oYBul-;iJ)!<83L@?VkI&C~&#i$PP*uh7wik0J( zS;^*B^J|6mVrgS@i)3t@%hbwjXuE7-djz!#VXV5}`gY##bX%QTr`_&a`)jxMp6B5Q tr@QCji9;M-JFz#d?&+t`wO3(FXqDb|PS3uapLSYj-H&=#1B@g#;Xk6bf06(I literal 0 HcmV?d00001 diff --git a/resources/server/transparent.png b/resources/server/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..02156f9b267003a3b0eacd994a407a0950002ce8 GIT binary patch literal 84 zcmeAS@N?&q;$mQ6;Pv!y2?EjrAk4uAB;`N^Bx4zL)cpV>5lEKr} K&t;ucLK6T|8z~Y1 literal 0 HcmV?d00001 diff --git a/scripts/common/bytetable.cs b/scripts/common/bytetable.cs new file mode 100644 index 0000000..2546445 --- /dev/null +++ b/scripts/common/bytetable.cs @@ -0,0 +1,225 @@ +// Converts integers to characters and back. Mainly used by save transfer. +// ------------------------------------------------------------------- + +//Binary compression (file version, 241 allowed characters) +/////////////////////////////////////////////////////////////////////////// + +//Creates byte lookup table +function ndCreateByte241Table() +{ + $ND::Byte241Lookup = ""; + + //This will map uints 0-241 to chars 15-255, starting after \r + for(%i = 15; %i < 256; %i++) + { + %char = collapseEscape("\\x" @ + getSubStr("0123456789abcdef", (%i & 0xf0) >> 4, 1) @ + getSubStr("0123456789abcdef", %i & 0x0f, 1)); + + $ND::Byte241ToChar[%i - 15] = %char; + $ND::Byte241Lookup = $ND::Byte241Lookup @ %char; + } + + $ND::Byte241TableCreated = true; +} + +//Packs uint in single byte +function ndPack241_1(%num) +{ + return $ND::Byte241ToChar[%num]; +} + +//Packs uint in two bytes +function ndPack241_2(%num) +{ + return $ND::Byte241ToChar[(%num / 241) | 0] @ $ND::Byte241ToChar[%num % 241]; +} + +//Packs uint in three bytes +function ndPack241_3(%num) +{ + return + $ND::Byte241ToChar[(((%num / 241) | 0) / 241) | 0] @ + $ND::Byte241ToChar[((%num / 241) | 0) % 241] @ + $ND::Byte241ToChar[%num % 241]; +} + +//Packs uint in four bytes +function ndPack241_4(%num) +{ + return + $ND::Byte241ToChar[(((((%num / 241) | 0) / 241) | 0) / 241) | 0] @ + $ND::Byte241ToChar[((((%num / 241) | 0) / 241) | 0) % 241] @ + $ND::Byte241ToChar[((%num / 241) | 0) % 241] @ + $ND::Byte241ToChar[%num % 241]; +} + +//Unpacks uint from single byte +function ndUnpack241_1(%subStr) +{ + return strStr($ND::Byte241Lookup, %subStr); +} + +//Unpacks uint from two bytes +function ndUnpack241_2(%subStr) +{ + return + strStr($ND::Byte241Lookup, getSubStr(%subStr, 0, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%subStr, 1, 1)); +} + +//Unpacks uint from three bytes +function ndUnpack241_3(%subStr) +{ + return + ((strStr($ND::Byte241Lookup, getSubStr(%subStr, 0, 1)) * 58081) | 0) + + strStr($ND::Byte241Lookup, getSubStr(%subStr, 1, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%subStr, 2, 1)); +} + +//Unpacks uint from four bytes +function ndUnpack241_4(%subStr) +{ + return + ((strStr($ND::Byte241Lookup, getSubStr(%subStr, 0, 1)) * 13997521) | 0) + + ((strStr($ND::Byte241Lookup, getSubStr(%subStr, 1, 1)) * 58081) | 0) + + strStr($ND::Byte241Lookup, getSubStr(%subStr, 2, 1)) * 241 + + strStr($ND::Byte241Lookup, getSubStr(%subStr, 3, 1)); +} + + + +//Binary compression (command version, 255 allowed characters) +/////////////////////////////////////////////////////////////////////////// + +//Creates byte lookup table +function ndCreateByte255Table() +{ + $ND::Byte255Lookup = ""; + + //This will map uints 0-254 to chars 1-255, starting after \x00 + for(%i = 1; %i < 256; %i++) + { + %char = collapseEscape("\\x" @ + getSubStr("0123456789abcdef", (%i & 0xf0) >> 4, 1) @ + getSubStr("0123456789abcdef", %i & 0x0f, 1)); + + $ND::Byte255ToChar[%i - 1] = %char; + $ND::Byte255Lookup = $ND::Byte255Lookup @ %char; + } + + $ND::Byte255TableCreated = true; +} + +//Packs uint in single byte +function ndPack255_1(%num) +{ + return $ND::Byte255ToChar[%num]; +} + +//Packs uint in two bytes +function ndPack255_2(%num) +{ + return $ND::Byte255ToChar[(%num / 255) | 0] @ $ND::Byte255ToChar[%num % 255]; +} + +//Packs uint in three bytes +function ndPack255_3(%num) +{ + return + $ND::Byte255ToChar[(((%num / 255) | 0) / 255) | 0] @ + $ND::Byte255ToChar[((%num / 255) | 0) % 255] @ + $ND::Byte255ToChar[%num % 255]; +} + +//Packs uint in four bytes +function ndPack255_4(%num) +{ + return + $ND::Byte255ToChar[(((((%num / 255) | 0) / 255) | 0) / 255) | 0] @ + $ND::Byte255ToChar[((((%num / 255) | 0) / 255) | 0) % 255] @ + $ND::Byte255ToChar[((%num / 255) | 0) % 255] @ + $ND::Byte255ToChar[%num % 255]; +} + +//Unpacks uint from single byte +function ndUnpack255_1(%subStr) +{ + return strStr($ND::Byte255Lookup, %subStr); +} + +//Unpacks uint from two bytes +function ndUnpack255_2(%subStr) +{ + return + strStr($ND::Byte255Lookup, getSubStr(%subStr, 0, 1)) * 255 + + strStr($ND::Byte255Lookup, getSubStr(%subStr, 1, 1)); +} + +//Unpacks uint from three bytes +function ndUnpack255_3(%subStr) +{ + return + ((strStr($ND::Byte255Lookup, getSubStr(%subStr, 0, 1)) * 65025) | 0) + + strStr($ND::Byte255Lookup, getSubStr(%subStr, 1, 1)) * 255 + + strStr($ND::Byte255Lookup, getSubStr(%subStr, 2, 1)) | 0; +} + +//Unpacks uint from four bytes +function ndUnpack255_4(%subStr) +{ + return + ((strStr($ND::Byte255Lookup, getSubStr(%subStr, 0, 1)) * 16581375) | 0) + + ((strStr($ND::Byte255Lookup, getSubStr(%subStr, 1, 1)) * 65025) | 0) + + strStr($ND::Byte255Lookup, getSubStr(%subStr, 2, 1)) * 255 + + strStr($ND::Byte255Lookup, getSubStr(%subStr, 3, 1)) | 0; +} + +//Some tests for the packing functions +function ndTestPack255() +{ + echo("Testing 1 byte"); + echo(ndUnpack255_1(ndPack255_1(0)) == 0); + echo(ndUnpack255_1(ndPack255_1(123)) == 123); + echo(ndUnpack255_1(ndPack255_1(231)) == 231); + echo(ndUnpack255_1(ndPack255_1(254)) == 254); + + echo("Testing 2 byte"); + echo(ndUnpack255_2(ndPack255_2(0)) == 0); + echo(ndUnpack255_2(ndPack255_2(123)) == 123); + echo(ndUnpack255_2(ndPack255_2(231)) == 231); + echo(ndUnpack255_2(ndPack255_2(254)) == 254); + echo(ndUnpack255_2(ndPack255_2(12345)) == 12345); + echo(ndUnpack255_2(ndPack255_2(32145)) == 32145); + echo(ndUnpack255_2(ndPack255_2(65024)) == 65024); + + echo("Testing 3 byte"); + echo(ndUnpack255_3(ndPack255_3(0)) == 0); + echo(ndUnpack255_3(ndPack255_3(123)) == 123); + echo(ndUnpack255_3(ndPack255_3(231)) == 231); + echo(ndUnpack255_3(ndPack255_3(254)) == 254); + echo(ndUnpack255_3(ndPack255_3(12345)) == 12345); + echo(ndUnpack255_3(ndPack255_3(32145)) == 32145); + echo(ndUnpack255_3(ndPack255_3(65024)) == 65024); + echo(ndUnpack255_3(ndPack255_3(11234567)) == 11234567); + echo(ndUnpack255_3(ndPack255_3(14132451)) == 14132451); + echo(ndUnpack255_3(ndPack255_3(16581374)) == 16581374); + + echo("Testing 4 byte"); + echo(ndUnpack255_4(ndPack255_4(0)) == 0); + echo(ndUnpack255_4(ndPack255_4(123)) == 123); + echo(ndUnpack255_4(ndPack255_4(231)) == 231); + echo(ndUnpack255_4(ndPack255_4(254)) == 254); + echo(ndUnpack255_4(ndPack255_4(12345)) == 12345); + echo(ndUnpack255_4(ndPack255_4(32145)) == 32145); + echo(ndUnpack255_4(ndPack255_4(65024)) == 65024); + echo(ndUnpack255_4(ndPack255_4(11234567)) == 11234567); + echo(ndUnpack255_4(ndPack255_4(14132451)) == 14132451); + echo(ndUnpack255_4(ndPack255_4(16581374)) == 16581374); + echo(ndUnpack255_4(ndPack255_4(1234567890)) == 1234567890); + + //Appearantly tork uses uint and normal int randomly in + //seperate places so we can't use the full uint range + echo(ndUnpack255_4(ndPack255_4(2147483647)) == 2147483647); + echo(ndUnpack255_4(ndPack255_4(2147483648)) != 2147483648); +} diff --git a/scripts/server/commands.cs b/scripts/server/commands.cs new file mode 100644 index 0000000..5837d0b --- /dev/null +++ b/scripts/server/commands.cs @@ -0,0 +1,1151 @@ +// General server commands used to control the new duplicator. +// ------------------------------------------------------------------- + +//Information commands +/////////////////////////////////////////////////////////////////////////// + +//Shows version of blockland and the new duplicator +function serverCmdDupVersion(%client) +{ + messageClient(%client, '', "\c6Blockland version: \c3r" @ getBuildNumber()); + messageClient(%client, '', "\c6New duplicator version: \c3" @ $ND::Version); + + if(%client.ndClient) + messageClient(%client, '', "\c6Your new duplicator version: \c3" @ %client.ndVersion); + else + messageClient(%client, '', "\c6You don't have the new duplicator installed"); +} + +//Shows versions of other clients +function serverCmdDupClients(%client) +{ + messageClient(%client, '', "\c6New duplicator versions:"); + + %cnt = ClientGroup.getCount(); + for(%i = 0; %i < %cnt; %i++) + { + %cl = ClientGroup.getObject(%i); + + if(%cl.ndClient) + messageClient(%client, '', "\c3" @ %cl.name @ "\c6 has \c3" @ %cl.ndVersion); + } +} + +//Shows list of commands +function serverCmdDupHelp(%client) +{ + messageClient(%client, '', " "); + messageClient(%client, '', "\c6You can use the following commands with your new duplicator:"); + messageClient(%client, '', "\c7--------------------------------------------------------------------------------"); + + messageClient(%client, '', "\c3/Duplicator\t\c6 Equip a new duplicator!"); + messageClient(%client, '', " "); + + messageClient(%client, '', "\c3/ForcePlant\t\c6 Plant a selection in mid air; bricks can float."); + messageClient(%client, '', "\c3/ToggleForcePlant\t\c6 Enable force plant for normal planting, so you dont have to type it all the time."); + messageClient(%client, '', "\c3/PlantAs\c6 [\c3target\c6]\t\c6 Plant bricks in a different brick group. Target can be a name or blid."); + messageClient(%client, '', " "); + + messageClient(%client, '', "\c3/FillWrench\t\c6 Open the fill wrench gui to change settings on all selected bricks."); + messageClient(%client, '', " "); + + messageClient(%client, '', "\c3/MirrorX\t\c6 Mirror your ghost selection left/right on screen."); + messageClient(%client, '', "\c3/MirrorY\t\c6 Mirror your ghost selection front/back on screen."); + messageClient(%client, '', "\c3/MirrorZ\t\c6 Mirror your ghost selection up/down on screen."); + messageClient(%client, '', "\c3/MirErrors\t\c6 List potential mirror errors after planting a mirrored ghost selection."); + messageClient(%client, '', " "); + + messageClient(%client, '', "\c3/SuperCut\t\c6 Delete everything in your selection box, cutting bricks in half on its sides!"); + messageClient(%client, '', "\c3/FillBricks\t\c6 First supercut, then completely fill your selection box with few bricks."); + messageClient(%client, '', " "); + + messageClient(%client, '', "\c3/SaveDup\c6 [\c3name\c6]\t\c6 Save your current selection to a file."); + messageClient(%client, '', "\c3/LoadDup\c6 [\c3name\c6]\t\c6 Load a selection from a file. Your current selection will be deleted."); + messageClient(%client, '', "\c3/AllDups\c6 [\c3filter\c6]\t\c6 Show all known saved duplications that match the filter. Leave blank to show all."); + messageClient(%client, '', " "); + + messageClient(%client, '', "\c3/DupVersion\t\c6 Show the duplicator and blockland versions running on the server."); + messageClient(%client, '', "\c3/DupClients\t\c6 Show the duplicator versions of other clients on the server."); + + messageClient(%client, '', "\c7--------------------------------------------------------------------------------"); + messageClient(%client, '', "\c6All of the commands can be shortened by just typing a \c3/\c6 and the capital letters!"); + messageClient(%client, '', "\c6You might have to use \c3PageUp\c6/\c3PageDown\c6 to see all of them."); + messageClient(%client, '', " "); +} + +//Alternative short commands +function serverCmdDV(%client){serverCmdDupVersion(%client);} +function serverCmdDC(%client){serverCmdDupClients(%client);} +function serverCmdDH(%client){serverCmdDupHelp(%client);} + + + +//Equip commands +/////////////////////////////////////////////////////////////////////////// + +//Command to equip the new duplicator +function serverCmdNewDuplicator(%client) +{ + //Check admin + if($Pref::Server::ND::AdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6The new duplicator is admin only. Ask an admin for help."); + return; + } + + //Check minigame + if(isObject(%client.minigame) && !%client.minigame.enablebuilding) + { + messageClient(%client, '', "\c6You cannot use the new duplicator while building is disabled in your minigame."); + return; + } + + //Check player + if(!isObject(%player = %client.player)) + { + messageClient(%client, '', "\c6You must be spawned to equip the new duplicator."); + return; + } + + //Hide brick selector and tool gui + %client.ndLastEquipTime = $Sim::Time; + commandToClient(%client, 'setScrollMode', 3); + + //Give player a duplicator + %image = %client.ndImage; + + if(!isObject(%image)) + %image = ND_Image; + + %player.updateArm(%image); + %player.mountImage(%image, 0); + %client.ndEquippedFromItem = false; +} + +//Alternative commands to equip the new duplicator (override old duplicators) +package NewDuplicator_Server_Final +{ + function serverCmdDuplicator(%client){serverCmdNewDuplicator(%client);} + function serverCmdDuplicato (%client){serverCmdNewDuplicator(%client);} + function serverCmdDuplicat (%client){serverCmdNewDuplicator(%client);} + function serverCmdDuplica (%client){serverCmdNewDuplicator(%client);} + function serverCmdDuplic (%client){serverCmdNewDuplicator(%client);} + function serverCmdDupli (%client){serverCmdNewDuplicator(%client);} + function serverCmdDupl (%client){serverCmdNewDuplicator(%client);} + function serverCmdDup (%client){serverCmdNewDuplicator(%client);} + function serverCmdDu (%client){serverCmdNewDuplicator(%client);} + function serverCmdD (%client){serverCmdNewDuplicator(%client);} +}; + + + +//Default keybind commands +/////////////////////////////////////////////////////////////////////////// +package NewDuplicator_Server +{ + //Light key (default: R) + function serverCmdLight(%client) + { + if(%client.ndModeIndex) + %client.ndMode.onLight(%client); + else + parent::serverCmdLight(%client); + } + + //Next seat (default: .) + function serverCmdNextSeat(%client) + { + if(%client.ndModeIndex) + %client.ndMode.onNextSeat(%client); + else + parent::serverCmdNextSeat(%client); + } + + //Previous seat (default: ,) + function serverCmdPrevSeat(%client) + { + if(%client.ndModeIndex) + %client.ndMode.onPrevSeat(%client); + else + parent::serverCmdPrevSeat(%client); + } + + //Shifting the ghost brick (default: numpad 2468/13/5+) + function serverCmdShiftBrick(%client, %x, %y, %z) + { + if(%client.ndModeIndex) + %client.ndMode.onShiftBrick(%client, %x, %y, %z); + + //Call parent to play animation + parent::serverCmdShiftBrick(%client, %x, %y, %z); + } + + //Super-shifting the ghost brick (default: alt numpad 2468/5+) + function serverCmdSuperShiftBrick(%client, %x, %y, %z) + { + if(%client.ndModeIndex) + %client.ndMode.onSuperShiftBrick(%client, %x, %y, %z); + + //Call parent to play animation + parent::serverCmdSuperShiftBrick(%client, %x, %y, %z); + } + + //Rotating the ghost brick (default: numpad 79) + function serverCmdRotateBrick(%client, %direction) + { + if(%client.ndModeIndex) + %client.ndMode.onRotateBrick(%client, %direction); + + //Call parent to play animation + parent::serverCmdRotateBrick(%client, %direction); + } + + //Undo bricks (default: ctrl z) + function serverCmdUndoBrick(%client) + { + if(%client.ndUndoInProgress) + { + messageClient(%client, '', "\c6Please wait for the current undo task to finish."); + return; + } + + //This really needs a better api. + //Wtf were you thinking, badspot? + %state = %client.undoStack.pop(); + %type = getField(%state, 1); + + if( + %type $= "ND_PLANT" + || %type $= "ND_PAINT" + || %type $= "ND_WRENCH" + ){ + %obj = getField(%state, 0); + if(isObject(%obj)){ + + if(%obj.brickCount > 10 && %client.ndUndoConfirm != %obj) + { + messageClient(%client, '', "\c6Next undo will affect \c3" @ %obj.brickCount @ "\c6 bricks. Press undo again to continue."); + %client.undoStack.push(%state); + %client.ndUndoConfirm = %obj; + return; + } + + %obj.ndStartUndo(%client); + + if(isObject(%client.player)) + %client.player.playThread(3, "undo"); + + %client.ndUndoConfirm = 0; + }else{ + talk("serverCmdUndoBrick(" @ %client.name @ ") - Nonexistent undo state " @ %state); + } + return; + } + + %client.ndUndoConfirm = 0; + %client.undoStack.push(%state); + parent::serverCmdUndoBrick(%client); + } +}; + +package NewDuplicator_Server_Final +{ + //Planting the ghost brick (default: numpad enter) + function serverCmdPlantBrick(%client) + { + if(%client.ndModeIndex) + %client.ndMode.onPlantBrick(%client); + else + parent::serverCmdPlantBrick(%client); + } + + //Removing the ghost brick (default: numpad 0) + function serverCmdCancelBrick(%client) + { + if(%client.ndModeIndex) + %client.ndMode.onCancelBrick(%client); + else + parent::serverCmdCancelBrick(%client); + } +}; + + + +//Custom keybind commands +/////////////////////////////////////////////////////////////////////////// + +//Copy selection (ctrl c) +function serverCmdNdCopy(%client) +{ + if(%client.ndModeIndex) + %client.ndMode.onCopy(%client); +} + +//Paste selection (ctrl v) +function serverCmdNdPaste(%client) +{ + if(%client.ndModeIndex) + %client.ndMode.onPaste(%client); +} + +//Cut selection (ctrl x) +function serverCmdNdCut(%client) +{ + if(%client.ndModeIndex) + %client.ndMode.onCut(%client); +} + +//Cut selection +function serverCmdCut(%client) +{ + serverCmdNdCut(%client); +} + +//Supercut selection +function serverCmdSuperCut(%client) +{ + if(%client.ndModeIndex != $NDM::BoxSelect) + { + messageClient(%client, '', "\c6Supercut can only be used on box selection mode."); + return; + } + + if(!isObject(%client.ndSelectionBox)) + { + messageClient(%client, '', "\c6Supercut can only be used with a selection box."); + return; + } + + if(%client.ndSelectionAvailable) + { + messageClient(%client, '', "\c6Supercut can not be used with any bricks selected."); + return; + } + + commandToClient(%client, 'messageBoxOkCancel', "New Duplicator | Supercut", + "Supercut is destructive and does\nNOT support undo at this time." @ + "\n\nPlease make sure the box is correct,\nthen press OK below.", + 'ndConfirmSuperCut'); +} + +//Confirm Supercut selection +function serverCmdNdConfirmSuperCut(%client) +{ + if(%client.ndModeIndex != $NDM::BoxSelect) + { + messageClient(%client, '', "\c6Supercut can only be used on box selection mode."); + return; + } + + if(!isObject(%client.ndSelectionBox)) + { + messageClient(%client, '', "\c6Supercut can only be used with a selection box."); + return; + } + + if(%client.ndSelectionAvailable) + { + messageClient(%client, '', "\c6Supercut can not be used with any bricks selected."); + return; + } + + %client.NDFillBrickSubset = $ND::SubsetDefault; + + %client.fillBricksAfterSuperCut = false; + %client.ndMode.onSuperCut(%client); +} + +//Alternative short command +function serverCmdSC(%client){serverCmdSuperCut(%client);} + +//Fill volume with bricks +function serverCmdFillBricks(%client, %subsetname) +{ + if($Pref::Server::ND::FillBricksAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Fill Bricks is admin only. Ask an admin for help."); + return; + } + + if(!isObject(%client.ndSelectionBox)) + { + messageClient(%client, '', "\c6The fillBricks command can only be used with a selection box."); + return; + } + + if(%client.ndSelectionAvailable) + { + messageClient(%client, '', "\c6The fillBricks command can not be used with any bricks selected."); + return; + } + + if(!%client.ndSelectionBox.hasVolume()) + { + messageClient(%client, '', "\c6The fillBricks command can only be used with a selection box that has a volume."); + return; + } + + %client.NDFillBrickSubset = (%subsetname !$= "") ? ndLookupSubsetName(%subsetname) : $ND::SubsetDefault; + + commandToClient(%client, 'messageBoxOkCancel', "New Duplicator | /FillBricks", + "/FillBricks will first do a Supercut\nbefore placing bricks, to fix overlap." @ + "\n\nSupercut is destructive and does\nNOT support undo at this time." @ + "\n\nPlease make sure the box is correct,\nthen press OK below to continue.", + 'ndConfirmFillBricks'); +} + +//Confirm fill volume with bricks +function serverCmdNdConfirmFillBricks(%client, %subsetname) +{ + if($Pref::Server::ND::FillBricksAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Fill Bricks is admin only. Ask an admin for help."); + return; + } + + if(!isObject(%client.ndSelectionBox)) + { + messageClient(%client, '', "\c6The fillBricks command can only be used with a selection box."); + return; + } + + if(%client.ndSelectionAvailable) + { + messageClient(%client, '', "\c6The fillBricks command can not be used with any bricks selected."); + return; + } + + if(!%client.ndSelectionBox.hasVolume()) + { + messageClient(%client, '', "\c6The fillBricks command can only be used with a selection box that has a volume."); + return; + } + + %client.NDFillBrickSubset = (%subsetname !$= "") ? + ndLookupSubsetName(%subsetname) : + ((%client.NDFillBrickSubset !$= "") ? + %client.NDFillBrickSubset : + $ND::SubsetDefault + ) + ; + + %client.fillBricksAfterSuperCut = true; + %client.ndMode.onSuperCut(%client); +} + +//Alternative short command +function serverCmdFB(%client){serverCmdFillBricks(%client);} +function serverCmdFBW(%client) { serverCmdFillBricks(%client, "LogicWire"); } + + +//MultiSelect toggle (ctrl) +function serverCmdNdMultiSelect(%client, %bool) +{ + %client.ndMultiSelect = !!%bool; + + if(%client.ndModeIndex == $NDM::StackSelect || %client.ndModeIndex == $NDM::BoxSelect) + %client.ndUpdateBottomPrint(); +} + + + +//Mirror commands +/////////////////////////////////////////////////////////////////////////// + +//Mirror selection on X relative to player +function serverCmdMirrorX(%client) +{ + if((getAngleIDFromPlayer(%client.getControlObject()) - %client.ndSelection.ghostAngleID) % 2 == 1) + %client.ndMirror(0); + else + %client.ndMirror(1); +} + +//Mirror selection on Y relative to player +function serverCmdMirrorY(%client) +{ + if((getAngleIDFromPlayer(%client.getControlObject()) - %client.ndSelection.ghostAngleID) % 2 == 1) + %client.ndMirror(1); + else + %client.ndMirror(0); +} + +//Mirror selection on Z +function serverCmdMirrorZ(%client) +{ + %client.ndMirror(2); +} + +//Alternative short commands +function serverCmdMX(%client){serverCmdMirrorX(%client);} +function serverCmdMY(%client){serverCmdMirrorY(%client);} +function serverCmdMZ(%client){serverCmdMirrorZ(%client);} + +//Attempt to mirror selection on axis +function GameConnection::ndMirror(%client, %axis) +{ + //Make sure symmetry table is created + if(!$ND::SymmetryTableCreated) + { + if(!$ND::SymmetryTableCreating) + ndCreateSymmetryTable(); + + messageClient(%client, '', "\c6Please wait for the symmetry table to finish, then mirror again."); + return; + } + + //If we're in plant mode, mirror the selection + if(isObject(%client.ndSelection) && %client.ndModeIndex == $NDM::PlantCopy) + { + %client.ndSelection.mirrorGhostBricks(%axis); + return; + } + + //If we have a ghost brick, mirror that instead + if(isObject(%client.player) && isObject(%client.player.tempBrick)) + { + %client.player.tempBrick.ndMirrorGhost(%client, %axis); + return; + } + + //We didn't mirror anything + messageClient(%client, '', "\c6The mirror command can only be used in plant mode or with a ghost brick."); +} + +//List potential mirror errors in last plant +function serverCmdMirErrors(%client) +{ + %xerr = $NS[%client, "MXC"]; + %zerr = $NS[%client, "MZC"]; + + if(%xerr) + { + messageClient(%client, '', " "); + messageClient(%client, '', "\c6These bricks are asymmetric and probably mirrored incorrectly:"); + + for(%i = 0; %i < %xerr; %i++) + { + %db = $NS[%client, "MXE", %i]; + messageClient(%client, '', "\c7 -" @ %i + 1 @ "- \c6" @ %db.category @ "/" @ %db.subCategory @ "/" @ %db.uiName); + } + } + + if(%zerr) + { + messageClient(%client, '', " "); + messageClient(%client, '', "\c6These bricks are not vertically symmetric and probably incorrect:"); + + for(%i = 0; %i < %zerr; %i++) + { + %db = $NS[%client, "MZE", %i]; + messageClient(%client, '', "\c7 -" @ %i + 1 @ "- \c6" @ %db.category @ "/" @ %db.subCategory @ "/" @ %db.uiName); + } + } + + if(!%xerr && !%zerr) + messageClient(%client, '', "\c6There were no mirror errors in your last plant attempt."); +} + +//Alternative short command +function serverCmdME(%client){serverCmdMirErrors(%client);} + + + +//Force plant +/////////////////////////////////////////////////////////////////////////// + +//Force plant one time +function serverCmdForcePlant(%client) +{ + //Check mode + if(%client.ndModeIndex != $NDM::PlantCopy) + { + messageClient(%client, '', "\c6Force Plant can only be used in Plant Mode."); + return; + } + + //Check admin + if($Pref::Server::ND::FloatAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Force Plant is admin only. Ask an admin for help."); + return; + } + + NDM_PlantCopy.conditionalPlant(%client, true); +} + +//Alternative short command +function serverCmdFP(%client){serverCmdForcePlant(%client);} + +//Keep force plant enabled +function serverCmdToggleForcePlant(%client) +{ + //Check admin + if($Pref::Server::ND::FloatAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Force Plant is admin only. Ask an admin for help."); + return; + } + + %client.ndForcePlant = !%client.ndForcePlant; + + if(%client.ndForcePlant) + messageClient(%client, '', "\c6Force Plant has been enabled. Use \c3/toggleForcePlant\c6 to disable it."); + else + messageClient(%client, '', "\c6Force Plant has been disabled. Use \c3/toggleForcePlant\c6 to enable it again."); +} + +//Alternative short command +function serverCmdTFP(%client){serverCmdToggleForcePlant(%client);} + + + +//Fill color +/////////////////////////////////////////////////////////////////////////// + +package NewDuplicator_Server +{ + //Enable fill color mode or show the current color + function serverCmdUseSprayCan(%client, %index) + { + %mode = %client.ndModeIndex; + + if(%mode == $NDM::StackSelect || %mode == $NDM::BoxSelect) + { + if(isObject(%client.ndSelection) && %client.ndSelection.brickCount) + { + %client.currentColor = %index; + %client.currentFxColor = ""; + %client.ndSetMode(NDM_FillColor); + return; + } + } + else if(%mode == $NDM::FillColor || %client.ndModeIndex == $NDM::FillColorProgress) + { + %client.currentColor = %index; + %client.currentFxColor = ""; + %client.ndUpdateBottomPrint(); + return; + } + + cancel(%client.ndToolSchedule); + parent::serverCmdUseSprayCan(%client, %index); + } + + //Enable fill color mode or show the current color + function serverCmdUseFxCan(%client, %index) + { + %mode = %client.ndModeIndex; + + if(%mode == $NDM::StackSelect || %mode == $NDM::BoxSelect) + { + if(isObject(%client.ndSelection) && %client.ndSelection.brickCount) + { + %client.currentFxColor = %index; + %client.ndSetMode(NDM_FillColor); + } + else + parent::serverCmdUseFxCan(%client, %index); + } + else if(%mode == $NDM::FillColor || %client.ndModeIndex == $NDM::FillColorProgress) + { + %client.currentFxColor = %index; + %client.ndUpdateBottomPrint(); + } + else + parent::serverCmdUseFxCan(%client, %index); + } +}; + + + +//Fill wrench +/////////////////////////////////////////////////////////////////////////// + +//Open the fill wrench gui +function serverCmdFillWrench(%client) +{ + //Check version + if(!%client.ndClient) + { + messageClient(%client, '', "\c6You need to have the new duplicator installed to use Fill Wrench."); + return; + } + + if(ndCompareVersion("1.2.0", %client.ndVersion) == 1) + { + messageClient(%client, '', "\c6Your version of the new duplicator is too old to use Fill Wrench."); + return; + } + + //Check admin + if($Pref::Server::ND::WrenchAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Fill Wrench is admin only. Ask an admin for help."); + return; + } + + //Check mode + if(%client.ndModeIndex != $NDM::StackSelect && %client.ndModeIndex != $NDM::BoxSelect) + { + messageClient(%client, '', "\c6Fill Wrench can only be used in Selection Mode."); + return; + } + + //Check selection + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + { + messageClient(%client, '', "\c6Fill Wrench can only be used with a selection."); + return; + } + + //Open fill wrench gui + commandToClient(%client, 'ndOpenWrenchGui'); +} + +//Short command +function serverCmdFW(%client) {serverCmdFillWrench(%client);} + +//Send data from gui +function serverCmdNdStartFillWrench(%client, %data) +{ + //Check admin + if($Pref::Server::ND::WrenchAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Fill Wrench is admin only. Ask an admin for help."); + return; + } + + //Check mode + if(%client.ndModeIndex != $NDM::StackSelect && %client.ndModeIndex != $NDM::BoxSelect) + { + messageClient(%client, '', "\c6Fill Wrench can only be used in Selection Mode."); + return; + } + + //Check selection + if(!isObject(%client.ndSelection) || !%client.ndSelection.brickCount) + { + messageClient(%client, '', "\c6Fill Wrench can only be used with a selection."); + return; + } + + //Change mode + %client.ndSetMode(NDM_WrenchProgress); + %client.ndSelection.startFillWrench(%data); +} + + + +//Saving and loading +/////////////////////////////////////////////////////////////////////////// + +package NewDuplicator_Server_Final +{ + //Save current selection to file + function serverCmdSaveDup(%client, %f0, %f1, %f2, %f3, %f4, %f5, %f6, %f7) + { + //Check timeout + if(!%client.isAdmin && %client.ndLastSaveTime + 10 > $Sim::Time) + { + %remain = mCeil(%client.ndLastSaveTime + 10 - $Sim::Time); + + if(%remain != 1) + %s = "s"; + + messageClient(%client, '', "\c6Please wait\c3 " @ %remain @ "\c6 second" @ %s @ " before saving again!"); + return; + } + + //Check admin + if($Pref::Server::ND::SaveAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Saving duplications is admin only. Ask an admin for help."); + return; + } + + //Check mode + if(%client.ndModeIndex != $NDM::PlantCopy) + { + messageClient(%client, '', "\c6Saving duplications can only be used in Plant Mode."); + return; + } + + //Filter file name + %allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ._-()"; + %fileName = trim(%f0 SPC %f1 SPC %f2 SPC %f3 SPC %f4 SPC %f5 SPC %f6 SPC %f7); + %filePath = $ND::ConfigPath @ "Saves/" @ %fileName @ ".bls"; + %filePath = strReplace(%filePath, ".bls.bls", ".bls"); + + for(%i = 0; %i < strLen(%fileName); %i++) + { + if(strStr(%allowed, getSubStr(%fileName, %i, 1)) == -1) + { + %forbidden = true; + break; + } + } + + if(%forbidden || !strLen(%fileName) || strLen(%fileName) > 50) + { + messageClient(%client, '', "\c6Bad save name \"\c3" @ %fileName @ "\c6\", please try again."); + messageClient(%client, '', "\c6Only \c3a-z A-Z 0-9 ._-()\c6 and \c3space\c6 are allowed, with a max length of 50 characters."); + return; + } + + //Check overwrite + if(isFile(%filePath) && %client.ndPotentialOverwrite !$= %fileName) + { + messageClient(%client, '', "\c6Save \"\c3" @ %fileName @ "\c6\" already exists. Repeat the command to overwrite."); + %client.ndPotentialOverwrite = %fileName; + return; + } + + %client.ndPotentialOverwrite = ""; + + //Check writeable + if(!isWriteableFileName(%filePath)) + { + messageClient(%client, '', "\c6File \"\c3" @ %fileName @ "\c6\" is not writeable. Ask the host for help."); + return; + } + + messageClient(%client, '', "\c6Saving selection to \"\c3" @ %fileName @ "\c6\"..."); + + //Notify admins + if(!%client.isAdmin) + { + for(%i = 0; %i < ClientGroup.getCount(); %i++) + { + %cl = ClientGroup.getObject(%i); + + if(%cl.isAdmin && %cl != %client) + messageClient(%cl, '', "\c3" @ %client.name @ "\c6 is saving duplication \"\c3" @ %fileName @ "\c6\""); + } + } + + //Write log + echo("ND: " @ %client.name @ " (" @ %client.bl_id @ ") is saving duplication \"" @ %fileName @ "\""); + + // Uncache saved file info + $ND::FileDate[%filePath] = ""; + + //Change mode + %client.ndSetMode(NDM_SaveProgress); + + if(!%client.ndSelection.startSaving(%filePath)) + { + messageClient(%client, '', "\c6Failed to write save \"\c3" @ %fileName @ "\c6\". Ask the host for help."); + %client.ndSetMode(NDM_PlantCopy); + } + } + + //Load selection from file + function serverCmdLoadDup(%client, %f0, %f1, %f2, %f3, %f4, %f5, %f6, %f7) + { + //Check timeout + if(!%client.isAdmin && %client.ndLastLoadTime + 5 > $Sim::Time) + { + %remain = mCeil(%client.ndLastLoadTime + 5 - $Sim::Time); + + if(%remain != 1) + %s = "s"; + + messageClient(%client, '', "\c6Please wait\c3 " @ %remain @ "\c6 second" @ %s @ " before loading again!"); + return; + } + + //Check admin + if($Pref::Server::ND::LoadAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Loading duplications is admin only. Ask an admin for help."); + return; + } + + //Attempt to get a duplicator + if(!%client.ndEquipped) + { + serverCmdNewDuplicator(%client); + + if(!%client.ndEquipped) + return; + } + + //Check mode + %mode = %client.ndModeIndex; + + if(%mode != $NDM::StackSelect && %mode != $NDM::BoxSelect && %mode != $NDM::PlantCopy) + { + messageClient(%client, '', "\c6Loading duplications can only be used in Plant or Selection Mode."); + return; + } + + //Filter file name + %allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ._-()"; + %fileName = trim(%f0 SPC %f1 SPC %f2 SPC %f3 SPC %f4 SPC %f5 SPC %f6 SPC %f7); + %filePath = $ND::ConfigPath @ "Saves/" @ %fileName @ ".bls"; + %filePath = strReplace(%filePath, ".bls.bls", ".bls"); + + for(%i = 0; %i < strLen(%fileName); %i++) + { + if(strStr(%allowed, getSubStr(%fileName, %i, 1)) == -1) + { + %forbidden = true; + break; + } + } + + if(%forbidden || !strLen(%fileName) || strLen(%fileName) > 50) + { + messageClient(%client, '', "\c6Bad save name \"\c3" @ %fileName @ "\c6\", please try again."); + messageClient(%client, '', "\c6Only \c3a-z A-Z 0-9 ._-()\c6 and \c3space\c6 are allowed, with a max length of 50 characters."); + return; + } + + //Check if file exists + if(!isFile(%filePath)) + { + messageClient(%client, '', "\c6Save \"\c3" @ %fileName @ "\c6\" does not exist, please try again."); + return; + } + + messageClient(%client, '', "\c6Loading selection from \"\c3" @ %fileName @ "\c6\"..."); + + //Notify admins + if(!%client.isAdmin) + { + for(%i = 0; %i < ClientGroup.getCount(); %i++) + { + %cl = ClientGroup.getObject(%i); + + if(%cl.isAdmin && %cl != %client) + messageClient(%cl, '', "\c3" @ %client.name @ "\c6 is loading duplication \"\c3" @ %fileName @ "\c6\""); + } + } + + //Write log + echo("ND: " @ %client.name @ " (" @ %client.bl_id @ ") is loading duplication \"" @ %fileName @ "\""); + + //Change mode + %client.ndSetMode(NDM_LoadProgress); + + if(!%client.ndSelection.startLoading(%filePath)) + { + messageClient(%client, '', "\c6Failed to read save \"\c3" @ %fileName @ "\c6\". Ask the host for help."); + %client.ndSetMode(%client.ndLastSelectMode); + } + } +}; + +function ND_SaveFileInfo(%filename) { + // Get date from file if not cached + if($ND::FileDate[%filename] $= "") { + %file = new FileObject(); + %file.openForRead(%filename); + %file.readLine(); + %file.readLine(); + %dateline = %file.readLine(); + %file.close(); + %file.delete(); + $ND::FileDate[%filename] = %dateline; + } + + // Construct table line + %info = $ND::FileDate[%filename]; + %info_aftername = getSubStr(%info, strStr(%info, " (")+2, strLen(%info)); + %date = getSubStr(%info_aftername, strLen(%info_aftername)-17, 17); + %blid = getSubStr(%info_aftername, 0, strStr(%info_aftername, ")")); + %name = getSubStr(%info, 9, strStr(%info, " (") - 9); + + // Fix date format for sorting + %sort = strReplace(strReplace(%date, "/", " "), ":", " "); + %sort = getWord(%sort, 2) SPC getWord(%sort, 0) SPC getWord(%sort, 1) SPC getWords(%sort, 3, 5) @ " "; + + %namepart = fileBase(%filename); + + %filetext = %sort @ + "\c3" @ %namepart TAB + "\c3" @ %name TAB + "\c6" @ %blid TAB + "\c6" @ %date + ; + + return %filetext; +} + +//Get list of all available dups +function serverCmdAllDups(%client, %pattern) +{ + //Check admin + if($Pref::Server::ND::LoadAdminOnly && !%client.isAdmin) + { + messageClient(%client, '', "\c6Loading duplications is admin only. Ask an admin for help."); + return; + } + + if(strLen(%pattern)) + { + //Filter pattern + %allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-()"; + %pattern = trim(%pattern); + + for(%i = 0; %i < strLen(%pattern); %i++) + { + if(strStr(%allowed, getSubStr(%pattern, %i, 1)) == -1) + { + messageClient(%client, '', "\c6Bad pattern \"\c3" @ %pattern @ "\c6\", please try again."); + messageClient(%client, '', "\c6Only \c3a-z A-Z 0-9 ._-()\c6 are allowed."); + return; + } + } + + %p = $ND::ConfigPath @ "Saves/*" @ %pattern @ "*.bls"; + } else { + %p = $ND::ConfigPath @ "Saves/*.bls"; + } + + //Get sorted list of files + %sort = new GuiTextListCtrl(); + + for(%filename = findFirstFile(%p); isFile(%filename); %filename = findNextFile(%p)) { + %info = ND_SaveFileInfo(%filename); + %sort.addRow(0, %info); + } + + %fileCount = %sort.rowCount(); + %sort.sort(0, true); + + //Dump list to client + if(%fileCount) + { + %s = (%fileCount == 1) ? " is" : "s are"; + + if(strLen(%pattern)) + messageClient(%client, '', "\c3" @ %fileCount @ "\c6 saved duplication" @ %s @ " available for filter \"\c3" @ %pattern @ "\c6\":"); + else + messageClient(%client, '', "\c3" @ %fileCount @ "\c6 saved duplication" @ %s @ " available:"); + + %format = ""; + if(%fileCount>0) { + messageClient(%client, '', %format @ "\c6Name\t\c6Saved By\t\c6BL_ID\t\c6Date"); + } + for(%i = 0; %i < %fileCount; %i++) { + %text = %sort.getRowText(%i); + messageClient(%client, '', %format @ getSubStr(%text, 20, strLen(%text))); + } + } + else + { + if(strLen(%pattern)) + messageClient(%client, '', "\c6No saved duplications are available for filter \"\c3" @ %pattern @ "\c6\"."); + else + messageClient(%client, '', "\c6No saved duplications are available."); + } + + %sort.delete(); + + messageClient(%client, '', "\c6Scroll using \c3PageUp\c6 and \c3PageDown\c6 if you can't see the whole list."); +} + +//Alternative short commands +function serverCmdSD(%client, %f0, %f1, %f2, %f3, %f4, %f5, %f6, %f7) {serverCmdSaveDup(%client, %f0, %f1, %f2, %f3, %f4, %f5, %f6, %f7);} +function serverCmdLD(%client, %f0, %f1, %f2, %f3, %f4, %f5, %f6, %f7) {serverCmdLoadDup(%client, %f0, %f1, %f2, %f3, %f4, %f5, %f6, %f7);} +function serverCmdAD(%client, %pattern) {serverCmdAllDups(%client, %pattern);} + + + +//Admin commands +/////////////////////////////////////////////////////////////////////////// + +//Cancel all active dups in case of spamming +function serverCmdClearDups(%client) +{ + if(!%client.isAdmin) + { + messageClient(%client, '', "\c6Canceling all duplicators is admin only. Ask an admin for help."); + return; + } + + messageAll('MsgClearBricks', "\c3" @ %client.getPlayerName() @ "\c0 canceled all duplicators."); + + for(%i = 0; %i < ClientGroup.getCount(); %i++) + { + %cl = ClientGroup.getObject(%i); + + if(%cl.ndModeIndex) + %cl.ndKillMode(); + } +} + +//Plant as a different brick group +function serverCmdPlantAs(%client, %t0, %t1, %t2, %t3, %t4) +{ + //Check mode + if(%client.ndModeIndex != $NDM::PlantCopy) + { + messageClient(%client, '', "\c6This command can only be used in Plant Mode."); + return; + } + + //Empty target to clear + if(!strLen(%t0)) + { + messageClient(%client, '', "\c6Bricks will be planted in your own group!"); + %client.ndSelection.targetGroup = ""; + %client.ndSelection.targetBlid = ""; + %client.ndUpdateBottomPrint(); + return; + } + + for(%i = 0; strLen(%t[%i]); %i++) + %target = %target SPC %t[%i]; + + //Attempt to find the brick group by name or blid + %target = trim(%target); + %targetClient = findClientByName(%target); + + if(!isObject(%targetClient)) + %targetClient = findClientByBL_ID(%target); + + if(isObject(%targetClient)) + %targetGroup = %targetClient.brickGroup; + else if((%target | 0) $= %target) + %targetGroup = nameToId("BrickGroup_" @ %target); + + if(!isObject(%targetGroup)) + { + messageClient(%client, '', "\c6No brick group was found for \"\c3" @ %target @ "\c6\"."); + messageClient(%client, '', "\c6Bricks will be planted in your own group!"); + %client.ndSelection.targetGroup = ""; + %client.ndSelection.targetBlid = ""; + %client.ndUpdateBottomPrint(); + return; + } + + //Check whether we have trust to the target group + if(getTrustLevel(%client, %targetGroup) < 1 && + (!%client.isAdmin || !$Pref::Server::ND::AdminTrustBypass2)) + { + messageClient(%client, '', "\c6You need build trust with \c3" + @ %targetGroup.name @ "\c6 to plant bricks in their group."); + + %client.ndSelection.targetGroup = ""; + %client.ndSelection.targetBlid = ""; + %client.ndUpdateBottomPrint(); + return; + } + + //Should be good to go + %name = %targetGroup.name; + if(getSubStr(%name, strLen(%name) - 1, 1) $= "s") + messageClient(%client, '', "\c6Bricks will now be planted in \c3" @ %name @ "\c6' group!"); + else + messageClient(%client, '', "\c6Bricks will now be planted in \c3" @ %name @ "\c6's group!"); + + %client.ndSelection.targetGroup = %targetGroup; + %client.ndSelection.targetBlid = %targetGroup.bl_id; + %client.ndUpdateBottomPrint(); +} + +//Alternative short command +function serverCmdPA(%client, %t0, %t1, %t2, %t3, %t4) {serverCmdPlantAs(%client, %t0, %t1, %t2, %t3, %t4);} diff --git a/scripts/server/datablocks.cs b/scripts/server/datablocks.cs new file mode 100644 index 0000000..4063005 --- /dev/null +++ b/scripts/server/datablocks.cs @@ -0,0 +1,344 @@ +// Creates datablocks for the handheld items and the selection box. +// ------------------------------------------------------------------- + +//Basic golden duplicator +/////////////////////////////////////////////////////////////////////////// + +//Duplicator Item +datablock ItemData(ND_Item) +{ + cameraMaxDist = 0.1; + canDrop = 1; + category = "Tools"; + className = "Weapon"; + density = 0.2; + doColorShift = false; + colorShiftColor = "1 0.84 0 1"; + elasticity = 0.2; + emap = 1; + friction = 0.6; + iconName = $ND::ResourcePath @ "server/icon"; + image = "ND_Image"; + shapeFile = $ND::ResourcePath @ "server/duplicator_brick.dts"; + uiName = "Duplicator"; +}; + +//Particles for explosion +datablock ParticleData(ND_HitParticle) +{ + colors[0] = "1 0.84 0 0.9"; + colors[1] = "1 0.84 0 0.7"; + colors[2] = "1 0.84 0 0.5"; + gravityCoefficient = 0.7; + lifetimeMS = 600; + lifetimeVarianceMS = 200; + sizes[0] = 0.6; + sizes[1] = 0.4; + sizes[2] = 0.3; + spinRandomMax = 90; + spinRandomMin = -90; + textureName = "base/client/ui/brickIcons/2x2"; + times[1] = 0.8; + times[2] = 1; +}; + +//Emitter for explosion +datablock ParticleEmitterData(ND_HitEmitter) +{ + lifetimeMS = 20; + ejectionPeriodMS = 1; + periodVarianceMS = 0; + ejectionVelocity = 3; + ejectionOffset = 0.2; + particles = ND_HitParticle; + thetaMin = 20; + thetaMax = 80; + velocityVariance = 0; +}; + +//Explosion +datablock ExplosionData(ND_HitExplosion) +{ + camShakeDuration = 0.5; + camShakeFreq = "1 1 1"; + emitter[0] = ND_HitEmitter; + faceViewer = 1; + lifetimeMS = 180; + lightEndRadius = 0; + lightStartColor = "0 0 0 0"; + lightStartRadius = 0; + shakeCamera = 1; + soundProfile = "wandHitSound"; +}; + +//Projectile to make explosion +datablock ProjectileData(ND_HitProjectile) +{ + bounceElasticity = 0; + bounceFriction = 0; + explodeOnDeath = 1; + explosion = ND_HitExplosion; + fadeDelay = 2; + gravityMod = 0; + lifetime = 0; + range = 10; +}; + +//Swing particles +datablock ParticleData(ND_WaitParticle) +{ + colors[0] = "1 0.84 0 0.9"; + colors[1] = "1 0.84 0 0.7"; + colors[2] = "1 0.84 0 0.5"; + gravityCoefficient = -0.4; + dragCoefficient = 2; + lifetimeMS = 400; + lifetimeVarianceMS = 200; + sizes[0] = 0.5; + sizes[1] = 0.8; + sizes[2] = 0; + spinRandomMax = 0; + spinRandomMin = 0; + textureName = "base/client/ui/brickIcons/1x1"; + times[1] = 0.5; + times[2] = 1; +}; + +//Swing emitter +datablock ParticleEmitterData(ND_WaitEmitter) +{ + lifetimeMS = 5000; + ejectionPeriodMS = 10; + periodVarianceMS = 0; + ejectionVelocity = 1; + ejectionOffset = 0.01; + particles = ND_WaitParticle; + thetaMin = 20; + thetaMax = 80; + velocityVariance = 0; +}; + +//Spin particles +datablock ParticleData(ND_SpinParticle : ND_WaitParticle) +{ + colors[0] = "1 0.65 0 0.9"; + colors[1] = "1 0.65 0 0.7"; + colors[2] = "1 0.65 0 0.5"; + gravityCoefficient = 0; + sizes[0] = 0.3; + sizes[1] = 0.5; + sizes[2] = 0; + textureName = "base/client/ui/brickIcons/1x1"; +}; + +//Spin emitter +datablock ParticleEmitterData(ND_SpinEmitter : ND_WaitEmitter) +{ + particles = ND_SpinParticle; + ejectionPeriodMS = 15; + thetaMin = 40; + thetaMax = 140; + ejectionVelocity = 2; +}; + +//Duplicator image +datablock ShapeBaseImageData(ND_Image) +{ + shapeFile = $ND::ResourcePath @ "server/duplicator_brick.dts"; + className = "WeaponImage"; + emap = true; + mountPoint = 0; + offset = "0 0 0"; + eyeOffset = "0.7 1.4 -0.9"; + armReady = true; + showBricks = true; + doColorShift = true; + colorShiftColor = "1 0.84 0 1"; + item = ND_Item; + projectile = ND_HitProjectile; + loaded = false; + + //Image states + stateName[0] = "Activate"; + stateSpinThread[0] = "Stop"; + stateTimeoutValue[0] = 0; + stateAllowImageChange[0] = false; + stateTransitionOnTimeout[0] = "Idle"; + + stateName[1] = "Idle"; + stateSpinThread[1] = "Stop"; + stateAllowImageChange[1] = true; + stateTransitionOnNotLoaded[1] = "StartSpinning"; + stateTransitionOnTriggerDown[1] = "PreFire"; + + stateName[2] = "PreFire"; + stateScript[2] = "onPreFire"; + stateTimeoutValue[2] = 0.01; + stateAllowImageChange[2] = false; + stateTransitionOnTimeout[2] = "Fire"; + + stateName[3] = "Fire"; + stateFire[3] = true; + stateScript[3] = "onFire"; + stateEmitter[3] = ND_WaitEmitter; + stateSequence[3] = "swing"; + stateEmitterNode[3] = "muzzlePoint"; + stateEmitterTime[3] = 0.01; + stateTimeoutValue[3] = 0.01; + stateWaitForTimeout[3] = true; + stateAllowImageChange[3] = false; + stateTransitionOnTimeout[3] = "CheckFire"; + + stateName[4] = "CheckFire"; + stateSpinThread[4] = "Stop"; + stateTransitionOnNotLoaded[4] = "StartSpinning_TDown"; + stateTransitionOnTriggerUp[4] = "Idle"; + + //Spinning states (from idle) + stateName[5] = "StartSpinning"; + stateSpinThread[5] = "SpinUp"; + stateTimeoutValue[5] = 0.25; + stateTransitionOnTimeout[5] = "IdleSpinning"; + + stateName[6] = "IdleSpinning"; + stateEmitter[6] = ND_SpinEmitter; + stateSpinThread[6] = "FullSpeed"; + stateEmitterNode[6] = "muzzlePoint"; + stateEmitterTime[6] = 0.35; + stateTimeoutValue[6] = 0.35; + stateTransitionOnLoaded[6] = "StopSpinning"; + stateTransitionOnTimeout[6] = "IdleSpinning"; + + stateName[7] = "StopSpinning"; + stateSpinThread[7] = "SpinDown"; + stateTimeoutValue[7] = 0.25; + stateTransitionOnTimeout[7] = "Idle"; + + //Spinning states (from checkfire, trigger is still down) + stateName[8] = "StartSpinning_TDown"; + stateSpinThread[8] = "SpinUp"; + stateTimeoutValue[8] = 0.25; + stateTransitionOnTimeout[8] = "IdleSpinning_TDown"; + + stateName[9] = "IdleSpinning_TDown"; + stateEmitter[9] = ND_SpinEmitter; + stateSpinThread[9] = "FullSpeed"; + stateEmitterNode[9] = "muzzlePoint_TDown"; + stateEmitterTime[9] = 0.4; + stateTimeoutValue[9] = 0.4; + stateTransitionOnLoaded[9] = "StopSpinning_TDown"; + stateTransitionOnTimeout[9] = "IdleSpinning_TDown"; + + stateName[10] = "StopSpinning_TDown"; + stateSpinThread[10] = "SpinDown"; + stateTimeoutValue[10] = 0.25; + stateTransitionOnTimeout[10] = "CheckFire"; +}; + + +//Spinning selection box for box mode +/////////////////////////////////////////////////////////////////////////// + +//Duplicator image +datablock ShapeBaseImageData(ND_Image_Box : ND_Image) +{ + shapeFile = $ND::ResourcePath @ "server/duplicator_selection.dts"; +}; + + +//Blue duplicator for plant mode +/////////////////////////////////////////////////////////////////////////// + +//Particles for explosion +datablock ParticleData(ND_HitParticle_Blue : ND_HitParticle) +{ + colors[0] = "0 0.25 1 0.9"; + colors[1] = "0 0.25 1 0.7"; + colors[2] = "0 0.25 1 0.5"; +}; + +//Emitter for explosion +datablock ParticleEmitterData(ND_HitEmitter_Blue : ND_HitEmitter) +{ + particles = ND_HitParticle_Blue; +}; + +//Explosion +datablock ExplosionData(ND_HitExplosion_Blue : ND_HitExplosion) +{ + emitter[0] = ND_HitEmitter_Blue; +}; + +//Projectile to make explosion +datablock ProjectileData(ND_HitProjectile_Blue : ND_HitProjectile) +{ + explosion = ND_HitExplosion_Blue; +}; + +//Swing particles +datablock ParticleData(ND_WaitParticle_Blue : ND_WaitParticle) +{ + colors[0] = "0 0.25 1 0.9"; + colors[1] = "0 0.25 1 0.7"; + colors[2] = "0 0.25 1 0.5"; +}; + +//Swing emitter +datablock ParticleEmitterData(ND_WaitEmitter_Blue : ND_WaitEmitter) +{ + particles = ND_WaitParticle_Blue; +}; + +//Spin particles +datablock ParticleData(ND_SpinParticle_Blue : ND_SpinParticle) +{ + colors[0] = "0 0.25 0.75 0.9"; + colors[1] = "0 0.25 0.75 0.7"; + colors[2] = "0 0.25 0.75 0.5"; +}; + +//Spin emitter +datablock ParticleEmitterData(ND_SpinEmitter_Blue : ND_SpinEmitter) +{ + particles = ND_SpinParticle_Blue; +}; + +//Duplicator image +datablock ShapeBaseImageData(ND_Image_Blue : ND_Image) +{ + colorShiftColor = "0 0.25 1 1"; + projectile = ND_HitProjectile_Blue; + + //Image states + stateEmitter[3] = ND_WaitEmitter_Blue; + stateEmitter[6] = ND_SpinEmitter_Blue; + stateEmitter[9] = ND_SpinEmitter_Blue; +}; + + +//Resizable selection and highlight box +/////////////////////////////////////////////////////////////////////////// + +//Transparent box to visualize bricks intersecting selection box +datablock StaticShapeData(ND_SelectionBoxOuter) +{ + shapeFile = $ND::ResourcePath @ "server/selectionbox_outer.dts"; +}; + +//Inside box (inverted normals) to visualize backfaces +datablock StaticShapeData(ND_SelectionBoxInner) +{ + shapeFile = $ND::ResourcePath @ "server/selectionbox_inner.dts"; +}; + +//Small box to create solid edges +datablock StaticShapeData(ND_SelectionBoxBorder) +{ + shapeFile = $ND::ResourcePath @ "server/selectionbox_border.dts"; +}; + +//Empty shape to hold shapename +datablock StaticShapeData(ND_SelectionBoxShapeName) +{ + shapeFile = "base/data/shapes/empty.dts"; +}; diff --git a/scripts/server/functions.cs b/scripts/server/functions.cs new file mode 100644 index 0000000..6decff2 --- /dev/null +++ b/scripts/server/functions.cs @@ -0,0 +1,734 @@ +// This file should not exist. Fix later... +// ------------------------------------------------------------------- + +//Math functions +/////////////////////////////////////////////////////////////////////////// + +//Rotate vector around +Z in 90 degree steps +function ndRotateVector(%vector, %steps) +{ + switch(%steps % 4) + { + case 0: return %vector; + case 1: return getWord(%vector, 1) SPC -getWord(%vector, 0) SPC getWord(%vector, 2); + case 2: return -getWord(%vector, 0) SPC -getWord(%vector, 1) SPC getWord(%vector, 2); + case 3: return -getWord(%vector, 1) SPC getWord(%vector, 0) SPC getWord(%vector, 2); + } +} + +//Rotate and mirror a direction +function ndTransformDirection(%dir, %steps, %mirrX, %mirrY, %mirrZ) +{ + if(%dir > 1) + { + if(%mirrX && %dir % 2 == 1 + || %mirrY && %dir % 2 == 0) + %dir += 2; + + %dir = (%dir + %steps - 2) % 4 + 2; + } + else if(%mirrZ) + %dir = !%dir; + + return %dir; +} + +//Get the closest paint color to an rgb value +function ndGetClosestColorID(%rgb) +{ + //Set initial value + %best = 0; + %bestDiff = 999999; + + for(%i = 0; %i < 64; %i++) + { + %color = getColorI(getColorIdTable(%i)); + + %diff = vectorLen(vectorSub(%rgb, %color)); + + if(getWord(%color, 3) != 255) + %diff += 1000; + + if(%diff < %bestDiff) + { + %best = %i; + %bestDiff = %diff; + } + } + + return %best; +} + +//Get the closest paint color to an rgba value +function ndGetClosestColorID2(%rgba) +{ + %rgb = getWords(%rgba, 0, 2); + %a = getWord(%rgba, 3); + + //Set initial value + %best = 0; + %bestDiff = 999999; + + for(%i = 0; %i < 64; %i++) + { + %color = getColorI(getColorIdTable(%i)); + %alpha = getWord(%color, 3); + + %diff = vectorLen(vectorSub(%rgb, %color)); + + if((%alpha > 254) != (%a > 254)) + %diff += 1000; + else + %diff += mAbs(%alpha - %a) * 0.5; + + if(%diff < %bestDiff) + { + %best = %i; + %bestDiff = %diff; + } + } + + return %best; +} + +//Convert a paint color to a code +function ndGetPaintColorCode(%id) +{ + %rgb = getColorI(getColorIdTable(%id)); + %chars = "0123456789abcdef"; + + %r = getWord(%rgb, 0); + %g = getWord(%rgb, 1); + %b = getWord(%rgb, 2); + + %r1 = getSubStr(%chars, (%r / 16) | 0, 1); + %r2 = getSubStr(%chars, %r % 16 , 1); + + %g1 = getSubStr(%chars, (%g / 16) | 0, 1); + %g2 = getSubStr(%chars, %g % 16 , 1); + + %b1 = getSubStr(%chars, (%b / 16) | 0, 1); + %b2 = getSubStr(%chars, %b % 16 , 1); + + return ""; +} + +//Get a plate world box from a raycast +function ndGetPlateBoxFromRayCast(%pos, %normal) +{ + //Get half size of world box for offset + %halfSize = "0.25 0.25 0.1"; + + //Point offset in correct direction based on normal + %offX = getWord(%halfSize, 0) * mFloatLength(-getWord(%normal, 0), 0); + %offY = getWord(%halfSize, 1) * mFloatLength(-getWord(%normal, 1), 0); + %offZ = getWord(%halfSize, 2) * mFloatLength(-getWord(%normal, 2), 0); + %offset = %offX SPC %offY SPC %offZ; + + //Get offset position + %newPos = vectorAdd(%pos, %offset); + + //Get the plate box around the position + %x1 = mFloor(getWord(%newPos, 0) * 2) / 2; + %y1 = mFloor(getWord(%newPos, 1) * 2) / 2; + %z1 = mFloor(getWord(%newPos, 2) * 5) / 5; + + %x2 = mCeil(getWord(%newPos, 0) * 2) / 2; + %y2 = mCeil(getWord(%newPos, 1) * 2) / 2; + %z2 = mCeil(getWord(%newPos, 2) * 5) / 5; + + return %x1 SPC %y1 SPC %z1 SPC %x2 SPC %y2 SPC %z2; +} + + + +//Trust checks +/////////////////////////////////////////////////////////////////////////// + +//Send a message if a client doesn't have select trust to a brick +function ndTrustCheckMessage(%obj, %client) +{ + %group = %client.brickGroup.getId(); + %bl_id = %client.bl_id; + %admin = %client.isAdmin; + + if(ndTrustCheckSelect(%obj, %group, %bl_id, %admin)) + return true; + + if(%obj.getGroup().bl_id == 888888 && !$Pref::Server::ND::SelectPublicBricks) + return false; + + messageClient(%client, 'MsgError', ""); + commandToClient(%client, 'centerPrint', "\c6You don't have enough trust to do that!", 5); + return false; +} + +//Check whether a client has enough trust to select a brick +function ndTrustCheckSelect(%obj, %group2, %bl_id, %admin) +{ + %group1 = %obj.getGroup(); + + //Client owns brick + if(%group1 == %group2) + return true; + + //Client owns stack + if(%obj.stackBL_ID == %bl_id) + return true; + + //Client has trust to the brick + if(%group1.Trust[%bl_id] >= $Pref::Server::ND::TrustLimit) + return true; + + //Client has trust to the stack of the brick + if(%group2.Trust[%obj.stackBL_ID] >= $Pref::Server::ND::TrustLimit) + return true; + + //Client is admin + if(%admin && $Pref::Server::ND::AdminTrustBypass1) + return true; + + //Client can duplicate public bricks + if(%group1.bl_id == 888888 && $Pref::Server::ND::SelectPublicBricks) + return true; + + return false; +} + +//Check whether a client has enough trust to modify a brick +function ndTrustCheckModify(%obj, %group2, %bl_id, %admin) +{ + %group1 = %obj.getGroup(); + + //Client owns brick + if(%group1 == %group2) + return true; + + //Client owns stack + if(%obj.stackBL_ID == %bl_id) + return true; + + //Client has trust to the brick + if(%group1.Trust[%bl_id] >= 2) + return true; + + //Client has trust to the stack of the brick + if(%group2.Trust[%obj.stackBL_ID] >= 2) + return true; + + //Client is admin + if(%admin && $Pref::Server::ND::AdminTrustBypass2) + return true; + + return false; +} + +//Fast check whether a client has enough trust to plant on a brick +function ndFastTrustCheck(%brick, %bl_id, %brickGroup) +{ + %group = %brick.getGroup(); + + if(%group == %brickGroup) + return true; + + if(%group.Trust[%bl_id] > 0) + return true; + + if(%group.bl_id == 888888) + return true; + + return false; +} + + + +//General stuff +/////////////////////////////////////////////////////////////////////////// + +//Setup list of spawned clients +function ndUpdateSpawnedClientList() +{ + $ND::NumSpawnedClients = 0; + + for(%i = 0; %i < ClientGroup.getCount(); %i++) + { + %cl = ClientGroup.getObject(%i); + + if(%cl.hasSpawnedOnce) + { + $ND::SpawnedClient[$ND::NumSpawnedClients] = %cl; + $ND::NumSpawnedClients++; + } + } +} + +//Applies mirror effect to a single ghost brick +function FxDtsBrick::ndMirrorGhost(%brick, %client, %axis) +{ + //Offset position + %bPos = %brick.position; + + //Rotated local angle id + %bAngle = %brick.angleID; + + //Apply mirror effects (ugh) + %datablock = %brick.getDatablock(); + + if(%axis == 0) + { + //Handle symmetries + switch($ND::Symmetry[%datablock]) + { + //Asymmetric + case 0: + if(%db = $ND::SymmetryXDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryXOffset[%datablock]) % 4; + + //Pair is made on X, so apply mirror logic for X afterwards + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + } + else + { + messageClient(%client, '', "\c6Sorry, your ghost brick is asymmetric and cannot be mirrored."); + return; + } + + //Do nothing for fully symmetric + + //X symmetric - rotate 180 degrees if brick is angled 90 or 270 degrees + case 2: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + + //Y symmetric - rotate 180 degrees if brick is angled 0 or 180 degrees + case 3: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + + //X+Y symmetric - rotate 90 degrees + case 4: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 1) % 4; + else + %bAngle = (%bAngle + 3) % 4; + + //X-Y symmetric - rotate -90 degrees + case 5: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 3) % 4; + else + %bAngle = (%bAngle + 1) % 4; + } + } + else if(%axis == 1) + { + //Handle symmetries + switch($ND::Symmetry[%datablock]) + { + //Asymmetric + case 0: + if(%db = $ND::SymmetryXDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryXOffset[%datablock]) % 4; + + //Pair is made on X, so apply mirror logic for X afterwards + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + } + else + { + messageClient(%client, '', "\c6Sorry, your ghost brick is asymmetric and cannot be mirrored."); + return; + } + + //Do nothing for fully symmetric + + //X symmetric - rotate 180 degrees if brick is angled 90 or 270 degrees + case 2: + if(%bAngle % 2 == 0) + %bAngle = (%bAngle + 2) % 4; + + //Y symmetric - rotate 180 degrees if brick is angled 0 or 180 degrees + case 3: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 2) % 4; + + //X+Y symmetric - rotate 90 degrees + case 4: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 1) % 4; + else + %bAngle = (%bAngle + 3) % 4; + + //X-Y symmetric - rotate -90 degrees + case 5: + if(%bAngle % 2 == 1) + %bAngle = (%bAngle + 3) % 4; + else + %bAngle = (%bAngle + 1) % 4; + } + } + else + { + //Change datablock if asymmetric + if(!$ND::SymmetryZ[%datablock]) + { + if(%db = $ND::SymmetryZDatablock[%datablock]) + { + %datablock = %db; + %bAngle = (%bAngle + $ND::SymmetryZOffset[%datablock]) % 4; + } + else + { + messageClient(%client, '', "\c6Sorry, your ghost brick is not vertically symmetric and cannot be mirrored."); + return; + } + } + } + + //Apply datablock + if(%brick.getDatablock() != %datablock) + %brick.setDatablock(%datablock); + + switch(%bAngle) + { + case 0: %bRot = "1 0 0 0"; + case 1: %bRot = "0 0 1 1.5708"; + case 2: %bRot = "0 0 1 3.14150"; + case 3: %bRot = "0 0 -1 1.5708"; + } + + //Apply transform + %brick.setTransform(%bPos SPC %bRot); +} + + + +//Supercut helpers +/////////////////////////////////////////////////////////////////////////// + +//Creates simple brick lookup table +function ndCreateSimpleBrickTable() +{ + deleteVariables("$ND::SimpleBrick*"); + %max = getDatablockGroupSize(); + + %file = new FileObject(); + %sorter = new GuiTextListCtrl(); + + for(%i = 0; %i < %max; %i++) + { + %db = getDatablock(%i); + + if(%db.getClassName() $= "FxDtsBrickData") + { + //Skip unsuitable bricks + if(( + %db.isWaterBrick + || %db.hasPrint + || %db.isSlyrBrick + || %db.uiName $= "" + || %db.ndDontUseForFill + || %db.category $= "Special" + || %db.isLogicGate + ) && ( + ndSubsetOfDatablock(%db)==$ND::SubsetDefault + ) + ){ + continue; + } + + %db.ndSubset = ndSubsetOfDatablock(%db); + + %file.openForRead(%db.brickFile); + %file.readLine(); + + //We only want simple bricks here + if(%file.readLine() $= "BRICK") + { + //Skip brick sizes that we already have + if(!$ND::SimpleBrickBlock[%db.brickSizeX, %db.brickSizeY, %db.BrickSizeZ, %db.ndSubset]){ + %sorter.addRow(%db, %db.getVolume()); + } + + $ND::SimpleBrickBlock[%db.brickSizeX, %db.brickSizeY, %db.BrickSizeZ, %db.ndSubset] = true; + } + + %file.close(); + } + } + + %file.delete(); + + //Sort the bricks by volume + %sorter.sortNumerical(0, 1); + + //Copy sorted bricks to global variable array + $ND::SimpleBrickCount = %sorter.rowCount(); + for(%i = 0; %i < $ND::SimpleBrickCount; %i++) + { + %db = %sorter.getRowId(%i); + %volume = %sorter.getRowText(%i); + + $ND::SimpleBrick[%i] = %db; + $ND::SimpleBrickVolume[%i] = %volume; + + $ND::SimpleBrickSubset[%i] = %db.ndSubset; + + //Ensure X < Y in lookup table + if(%db.brickSizeX <= %db.brickSizeY) + { + $ND::SimpleBrickSizeX[%i] = %db.brickSizeX; + $ND::SimpleBrickSizeY[%i] = %db.brickSizeY; + $ND::SimpleBrickRotated[%i] = false; + } + else + { + $ND::SimpleBrickSizeX[%i] = %db.brickSizeY; + $ND::SimpleBrickSizeY[%i] = %db.brickSizeX; + $ND::SimpleBrickRotated[%i] = true; + } + + $ND::SimpleBrickSizeZ[%i] = %db.brickSizeZ; + } + + %sorter.delete(); + $ND::SimpleBrickTableCreated = true; +} + +//Find the largest (volume) brick that fits inside the area +function ndGetLargestBrickId(%x, %y, %z, %subset) +{ + if(!$ND::SimpleBrickTableCreated) + ndCreateSimpleBrickTable(); + + %maxVolume = %x * %y * %z; + %start = $ND::SimpleBrickCount - 1; + + if($ND::SimpleBrickVolume[%start] > %maxVolume) + { + //Use binary search to find the largest volume that + //is smaller or equal to the volume of the area + %bound1 = 0; + %bound2 = %start; + + while(%bound1 < %bound2) + { + %i = mCeil((%bound1 + %bound2) / 2); + %volume = $ND::SimpleBrickVolume[%i]; + + if(%volume > %maxVolume) + { + %bound2 = %i - 1; + continue; + } + + if(%volume <= %maxVolume) + { + %bound1 = %i + 1; + continue; + } + } + + %start = %bound2; + } + + %bestIndex = -1; + + //Go down the list until a brick fits on all 3 axis + for(%i = %start; %i >= 0; %i--) + { + if( + $ND::SimpleBrickSizeX[%i] <= %x + && $ND::SimpleBrickSizeY[%i] <= %y + && $ND::SimpleBrickSizeZ[%i] <= %z + && $ND::SimpleBrickSubset[%i] == %subset + ) { + return %i; + } + } + + return -1; +} + +//Fill an area with bricks +function ndFillAreaWithBricks(%pos1, %pos2) +{ + %pos1_x = getWord(%pos1, 0); + %pos1_y = getWord(%pos1, 1); + %pos1_z = getWord(%pos1, 2); + + %pos2_x = getWord(%pos2, 0); + %pos2_y = getWord(%pos2, 1); + %pos2_z = getWord(%pos2, 2); + + %size_x = %pos2_x - %pos1_x; + %size_y = %pos2_y - %pos1_y; + %size_z = %pos2_z - %pos1_z; + + if(%size_x < 0.05 || %size_y < 0.05 || %size_z < 0.05) + return; + + if(%size_x > %size_y) + { + %tmp = %size_y; + %size_y = %size_x; + %size_x = %tmp; + %rotated = true; + } + + %brickId = ndGetLargestBrickId(%size_x * 2 + 0.05, %size_y * 2 + 0.05, %size_z * 5 + 0.02, $ND::FillBrickSubset); + + if(!%rotated) + { + %pos3_x = %pos1_x + $ND::SimpleBrickSizeX[%brickId] / 2; + %pos3_y = %pos1_y + $ND::SimpleBrickSizeY[%brickId] / 2; + } + else + { + %pos3_x = %pos1_x + $ND::SimpleBrickSizeY[%brickId] / 2; + %pos3_y = %pos1_y + $ND::SimpleBrickSizeX[%brickId] / 2; + } + + %pos3_z = %pos1_z + $ND::SimpleBrickSizeZ[%brickId] / 5; + %plantPos = (%pos1_x + %pos3_x) / 2 SPC (%pos1_y + %pos3_y) / 2 SPC (%pos1_z + %pos3_z) / 2; + + if(!isObject($ND::SimpleBrick[%brickId])) + return; + + %brick = new FxDTSBrick() + { + datablock = $ND::SimpleBrick[%brickId]; + isPlanted = true; + client = $ND::FillBrickClient; + + position = %plantPos; + rotation = (%rotated ^ $ND::SimpleBrickRotated[%brickId]) ? "0 0 1 90.0002" : "1 0 0 0"; + angleID = %rotated; + + colorID = $ND::FillBrickColorID; + colorFxID = $ND::FillBrickColorFxID; + shapeFxID = $ND::FillBrickShapeFxID; + + printID = 0; + }; + + //This will call ::onLoadPlant instead of ::onPlant + %prev1 = $Server_LoadFileObj; + %prev2 = $LastLoadedBrick; + $Server_LoadFileObj = %brick; + $LastLoadedBrick = %brick; + + //Add to brickgroup + $ND::FillBrickGroup.add(%brick); + + //Attempt plant + %error = %brick.plant(); + + //Restore variable + $Server_LoadFileObj = %prev1; + $LastLoadedBrick = %prev2; + + if(!%error || %error == 2) + { + //Set trusted + if(%brick.getNumDownBricks()) + %brick.stackBL_ID = %brick.getDownBrick(0).stackBL_ID; + else if(%brick.getNumUpBricks()) + %brick.stackBL_ID = %brick.getUpBrick(0).stackBL_ID; + else + %brick.stackBL_ID = $ND::FillBrickBL_ID; + + %brick.trustCheckFinished(); + + %brick.setRendering($ND::FillBrickRendering); + %brick.setColliding($ND::FillBrickColliding); + %brick.setRayCasting($ND::FillBrickRayCasting); + + //Instantly ghost the brick to all spawned clients (wow hacks) + for(%j = 0; %j < $ND::NumSpawnedClients; %j++) + { + %cl = $ND::SpawnedClient[%j]; + %brick.scopeToClient(%cl); + %brick.clearScopeToClient(%cl); + } + + $ND::FillBrickCount++; + } + else + %brick.delete(); + + if((%pos3_x + 0.05) < %pos2_x) + ndFillAreaWithBricks(%pos3_x SPC %pos1_y SPC %pos1_z, %pos2_x SPC %pos2_y SPC %pos2_z); + + if((%pos3_y + 0.05) < %pos2_y) + ndFillAreaWithBricks(%pos1_x SPC %pos3_y SPC %pos1_z, %pos3_x SPC %pos2_y SPC %pos2_z); + + if((%pos3_z + 0.02) < %pos2_z) + ndFillAreaWithBricks(%pos1_x SPC %pos1_y SPC %pos3_z, %pos3_x SPC %pos3_y SPC %pos2_z); + +} + +//Client finished supercut, now fill bricks +function GameConnection::doFillBricks(%this, %subset) +{ + //Set variables for the fill brick function + $ND::FillBrickGroup = %this.brickGroup; + $ND::FillBrickClient = %this; + $ND::FillBrickBL_ID = %this.bl_id; + + $ND::FillBrickColorID = %this.currentColor; + $ND::FillBrickColorFxID = 0; + $ND::FillBrickShapeFxID = 0; + + $ND::FillBrickRendering = true; + $ND::FillBrickColliding = true; + $ND::FillBrickRayCasting = true; + + $ND::FillBrickSubset = %subset; + + %box = %this.ndSelectionBox.getWorldBox(); + $ND::FillBrickCount = 0; + ndUpdateSpawnedClientList(); + ndFillAreaWithBricks(getWords(%box, 0, 2), getWords(%box, 3, 5)); + + //%s = ($ND::FillBrickCount == 1 ? "" : "s"); + //messageClient(%this, '', "\c6Filled in \c3" @ $ND::FillBrickCount @ "\c6 brick" @ %s); +} + +$ND::SubsetDefault = 0; +$ND::SubsetLogicWire = 1; +$ND::SubsetLogicBuffer = 2; +$ND::SubsetLogicBufferAl = 3; +$ND::SubsetLogicDff = 4; +$ND::SubsetLogicDffAl = 5; +$ND::SubsetLogicEnabler = 6; +$ND::SubsetLogicEnablerAl = 7; + +// Which subset of fill bricks to use - normal or wire +function ndSubsetOfDatablock(%data){ + if(%data.isLogic) { + if(%data.isLogicWire) { + return $ND::SubsetLogicWire; + } else if(strStr(%data.uiName, "Buffer") == 0) { + return (strStr(%data.uiName, "Active Low")==-1) ? $ND::SubsetLogicBuffer : $ND::SubsetLogicBufferAl; + } else if(strStr(%data.uiName, "D FlipFlop") == 0) { + return (strStr(%data.uiName, "Active Low")==-1) ? $ND::SubsetLogicDff : $ND::SubsetLogicDffAl; + }else if(strStr(%data.uiName, "Enabler") == 0) { + return (strStr(%data.uiName, "Active Low")==-1) ? $ND::SubsetLogicEnabler : $ND::SubsetLogicEnablerAl; + } else { + return $ND::SubsetDefault; + } + } else { + return $ND::SubsetDefault; + } +} + +function ndLookupSubsetName(%name) { + %subset = $ND::Subset[%name]; + return (%subset !$= "") ? %subset : $ND::SubsetDefault; +} diff --git a/scripts/server/handshake.cs b/scripts/server/handshake.cs new file mode 100644 index 0000000..2108f6c --- /dev/null +++ b/scripts/server/handshake.cs @@ -0,0 +1,71 @@ +// Sends a handshake to new clients to obtain their duplicator version. +// ------------------------------------------------------------------- + +package NewDuplicator_Server +{ + //Send handshake request to new client + function GameConnection::autoAdminCheck(%this) + { + %this.ndClient = false; + %this.ndVersion = "0.0.0"; + + commandToClient(%this, 'ndHandshake', $ND::Version); + return parent::autoAdminCheck(%this); + } +}; + +//Client responded, so he has new duplicator +function serverCmdNdHandshake(%this, %version) +{ + cancel(%this.ndHandshakeTimeout); + + %this.ndClient = true; + %this.ndVersion = %version; + + //Inform client whether he has an outdated version + switch(ndCompareVersion($ND::Version, %version)) + { + case 1: + %m = "\c6Your version of the \c3New Duplicator\c6 is outdated! Some features might not work. "; + %m = %m @ "(Server Version: \c3" @ $ND::Version @ "\c6 | Your Version: \c0" @ %version @ "\c6)"; + //messageClient(%this, '', %m); + + case 2: + //Hide this message on long-running dedicated servers + if($Sim::Time < 86400) + { + %m = "\c6Your version of the \c3New Duplicator\c6 is newer than the server's! Ask the host to update it! "; + %m = %m @ "(Server Version: \c0" @ $ND::Version @ "\c6 | Your Version: \c3" @ %version @ "\c6)"; + //messageClient(%this, '', %m); + } + } +} + +//Compares two version numbers (major.minor.patch) +function ndCompareVersion(%ver1, %ver2) +{ + %ver1 = strReplace(%ver1, ".", " "); + %ver2 = strReplace(%ver2, ".", " "); + + %count = getMax(getWordCount(%ver1), getWordCount(%ver2)); + + for(%i = 0; %i < %count; %i ++) + { + %v1 = getWord(%ver1, %i); + %v2 = getWord(%ver2, %i); + + if(%v1 > %v2) + return 1; + else if(%v1 < %v2) + return 2; + } + + return 0; +} + +//Send handshakes to all clients +function ndResendHandshakes() +{ + for(%i = 0; %i < ClientGroup.getCount(); %i++) + commandToClient(ClientGroup.getObject(%i), 'ndHandshake', $ND::Version); +} diff --git a/scripts/server/highlight.cs b/scripts/server/highlight.cs new file mode 100644 index 0000000..1ca99da --- /dev/null +++ b/scripts/server/highlight.cs @@ -0,0 +1,106 @@ +// Handles highlighting and de-highlighting large groups of bricks. +// ------------------------------------------------------------------- + +//Highlight group data $NDH::* +// $NDH::LastId : The id of the last created highlight group +// $NDH::Count : Total number of active highlight groups +// +// $NDHN[brick] : Number of groups a brick is in +// $NDHF[brick] : Original color fx of the brick +// +// $NDH[group] : Count of bricks in a group +// $NDH[group, i] : Brick in group at position i + +//Reserve a highlight group id +function ndNewHighlightGroup() +{ + //Increase group number + $NDH::LastId++; + $NDH::Count++; + + //Set initial count + $NDH[$NDH::LastId] = 0; + + //Assign free id + return $NDH::LastId; +} + +//Remove highlight group and clean up garbage variables +function ndRemoveHighlightGroup(%group) +{ + //Don't delete groups that don't exist + if($NDH[%group] $= "") + return; + + //Lower group number + $NDH::Count--; + + //Clear count to allow reuse of index + $NDH[%group] = ""; + + //Cancel schedules + cancel($NDHS[%group]); + + //If this is the most recently created group, pretend it never existed + if($NDH::LastId == %group) + $NDH::LastId--; + + //If this is the last highlight group, just delete ALL highlight variables + if($NDH::Count < 1) + deleteVariables("$NDH*"); +} + +//Add a brick to a highlight group +function ndHighlightBrick(%group, %brick) +{ + //If brick is not highlighted, do that + if(!$NDHN[%brick]) + { + $NDHF[%brick] = %brick.colorFxID; + %brick.setColorFx(3); + } + + //Increase group number of this brick + $NDHN[%brick]++; + + //Add brick to highlight group + $NDH[%group, ($NDH[%group]++) - 1] = %brick; +} + +//Start de-highlighting bricks +function ndStartDeHighlight(%group) +{ + //Don't do this if already de-highlighting + %t = getTimeRemaining($NDHS[%group]); + + if(%t > 66 || %t == 0) + { + cancel($NDHS[%group]); + ndTickDeHighlight(%group, 0); + } +} + +//Tick de-highlighting bricks +function ndTickDeHighlight(%group, %start) +{ + %end = $NDH[%group]; + + if(%end - %start > $Pref::Server::ND::ProcessPerTick) + %end = %start + $Pref::Server::ND::ProcessPerTick; + else + %lastTick = true; + + for(%i = %start; %i < %end; %i++) + { + %brick = $NDH[%group, %i]; + + //If the brick is in no more groups, de-highlight it + if(isObject(%brick) && !($NDHN[%brick]--)) + %brick.setColorFx($NDHF[%brick]); + } + + if(!%lastTick) + $NDHS[%group] = schedule(30, 0, ndTickDeHighlight, %group, %end); + else + ndRemoveHighlightGroup(%group); +} diff --git a/scripts/server/images.cs b/scripts/server/images.cs new file mode 100644 index 0000000..085b42a --- /dev/null +++ b/scripts/server/images.cs @@ -0,0 +1,297 @@ +// Handles interactions with the handheld duplicator item. +// ------------------------------------------------------------------- + +//Set which image a client should use +function GameConnection::ndSetImage(%this, %image) +{ + %image = %image.getId(); + + if(%image != %this.ndImage) + { + %this.ndImage = %image; + + if(%this.ndEquipped) + { + %this.ndIgnoreNextMount = true; + %this.player.schedule(0, updateArm, %image); + %this.player.schedule(0, mountImage, %image, 0); + } + } +} + +//Mount the correct image when the item is equipped +function ND_Item::onUse(%this, %player, %slot) +{ + %image = %player.client.ndImage; + + if(!isObject(%image)) + %image = ND_Image; + + %player.updateArm(%image); + %player.mountImage(%image, 0); + %player.client.ndEquippedFromItem = true; +} + +package NewDuplicator_Server +{ + //Start select mode when duplicator is equipped + function ND_Image::onMount(%this, %player, %slot) + { + parent::onMount(%this, %player, %slot); + %player.ndEquipped(); + } + + function ND_Image_Blue::onMount(%this, %player, %slot) + { + parent::onMount(%this, %player, %slot); + %player.ndEquipped(); + } + + function ND_Image_Box::onMount(%this, %player, %slot) + { + parent::onMount(%this, %player, %slot); + %player.ndEquipped(); + } + + //Cancel mode when duplicator is unequipped + function ND_Image::onUnMount(%this, %player, %slot) + { + parent::onUnMount(%this, %player, %slot); + %player.ndUnEquipped(); + } + + function ND_Image_Blue::onUnMount(%this, %player, %slot) + { + parent::onUnMount(%this, %player, %slot); + %player.ndUnEquipped(); + } + + function ND_Image_Box::onUnMount(%this, %player, %slot) + { + parent::onUnMount(%this, %player, %slot); + %player.ndUnEquipped(); + } +}; + +//Start the swinging animation +function ND_Image::onPreFire(%this, %player, %slot) +{ + %player.playThread(2, shiftTo); +} + +function ND_Image_Blue::onPreFire(%this, %player, %slot) +{ + %player.playThread(2, shiftTo); +} + +function ND_Image_Box::onPreFire(%this, %player, %slot) +{ + %player.playThread(2, shiftTo); +} + +//Handle selecting things +function ND_Image::onFire(%this, %player, %slot) +{ + %player.ndFired(); +} + +function ND_Image_Blue::onFire(%this, %player, %slot) +{ + %player.ndFired(); +} + +function ND_Image_Box::onFire(%this, %player, %slot) +{ + %player.ndFired(); +} + +//Duplicator was equipped +function Player::ndEquipped(%this) +{ + %client = %this.client; + + if(%this.isHoleBot || %this.isSlayerBot || !isObject(%client)) + return; + + if(%client.ndIgnoreNextMount) + { + %client.ndIgnoreNextMount = false; + return; + } + + if($Pref::Server::ND::AdminOnly && !%client.isAdmin) + { + commandToClient(%client, 'centerPrint', "\c6Oops! The duplicator is admin only.", 5); + return; + } + + %client.ndEquipped = true; + + //Remove temp brick so it doesn't overlap the selection box + if(isObject(%this.tempBrick)) + %this.tempBrick.delete(); + + //Should resume last used select mode + if(!%client.ndModeIndex) + %client.ndSetMode(%client.ndLastSelectMode); +} + +//Duplicator was unequipped +function Player::ndUnEquipped(%this) +{ + %client = %this.client; + + if(%this.isHoleBot || %this.isSlayerBot || !isObject(%client)) + return; + + if(%client.ndIgnoreNextMount) + return; + + if(%client.ndModeIndex && !%client.ndMode.allowUnMount) + %client.ndKillMode(); + + %client.ndEquipped = false; +} + +//Duplicator was fired +function Player::ndFired(%this) +{ + %client = %this.client; + + if(!isObject(%client) || !%client.ndModeIndex || !%client.ndMode.allowSelecting) + return; + + if(isObject(%client.minigame) && !%client.minigame.enablebuilding) + { + commandToClient(%client, 'centerPrint', "\c6Oops! Building is disabled.", 5); + return; + } + + //Support for Script_RaycastOffTools by Conan + if(%this.isRaycastTool) + %mask = $TypeMasks::FxBrickObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::InteriorObjectType; + else + %mask = $TypeMasks::FxBrickAlwaysObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::InteriorObjectType; + + %start = %this.getEyePoint(); + %dir = %this.getEyeVector(); + + //Octree::Raycast fails to detect close bricks (~1TU) with a long raycast, so we'll do 3. + //First a very short one of 1 TU to detect very close bricks, then a slightly longer one + //of 10 TU, and finally the long range one of 1000. + %len[0] = 1; + %len[1] = 10; + %len[2] = 1000; + + for(%i = 0; %i < 3; %i++) + { + %end = vectorAdd(%start, vectorScale(%dir, %len[%i])); + %ray = containerRaycast(%start, %end, %mask, %this); + + if(isObject(%obj = firstWord(%ray))) + break; + } + + if(!isObject(%obj)) + return; + + %position = posFromRaycast(%ray); + %normal = normalFromRaycast(%ray); + + //Can't directly spawn an explosion, must use a projectile + %data = %client.ndImage.projectile; + + if(!isObject(%data)) + %data = ND_HitProjectile; + + %proj = new Projectile() + { + datablock = %data; + initialPosition = %position; + initialVelocity = %normal; + }; + + //Pass on the selected object to the dupli mode + %client.ndMode.onSelectObject(%client, %obj, %position, %normal); +} + +package NewDuplicator_Server +{ + //Automatically start the "ambient" animation on duplicator items + function ND_Item::onAdd(%this, %obj) + { + parent::onAdd(%this, %obj); + %obj.playThread(0, ambient); + + //Fix colorshift bullshit + %obj.schedule(100, setNodeColor, "ALL", %this.colorShiftColor); + } + + //Prevent accidently unequipping the duplicator + function serverCmdUnUseTool(%client) + { + if(%client.ndLastEquipTime + 1.5 > $Sim::Time) + return; + + if(%client.ndModeIndex == $NDM::StackSelect || %client.ndModeIndex == $NDM::BoxSelect) + { + %client.ndToolSchedule = %client.schedule(100, ndUnUseTool); + return; + } + + parent::serverCmdUnUseTool(%client); + } + + //Prevent creating ghost bricks in modes that allow un-mount + function BrickDeployProjectile::onCollision(%this, %obj, %col, %fade, %pos, %normal) + { + %client = %obj.client; + + if(isObject(%client) && %client.ndModeIndex) + %client.ndMode.onSelectObject(%client, %col, %pos, %normal); + else + parent::onCollision(%this, %obj, %col, %fade, %pos, %normal); + } + + //Handle ghost brick movements from New Brick Tool + function placeNewGhostBrick(%client, %pos, %normal, %noOrient) + { + if(!isObject(%client) || !%client.ndModeIndex) + return parent::placeNewGhostBrick(%client, %pos, %normal, %noOrient); + + if(%client.ndModeIndex == $NDM::PlantCopy) + { + if(!%noOrient) + { + %angleID = getAngleIDFromPlayer(%client.getControlObject()) - %client.ndSelection.angleIDReference; + %rotation = ((4 - %angleID) - %client.ndSelection.ghostAngleID) % 4; + + if(%rotation != 0) + %client.ndSelection.rotateGhostBricks(%rotation, %client.ndPivot); + } + + return NDM_PlantCopy.moveBricksTo(%client, %pos, %normal); + } + + %client.ndMode.onSelectObject(%client, 0, %pos, %normal); + } +}; + +//Fix for equipping paint can calling unUseTool +function GameConnection::ndUnUseTool(%this) +{ + %player = %this.player; + + if(%this.isTalking) + serverCmdStopTalking(%this); + + if(!isObject(%player)) + return; + + %player.currTool = -1; + %this.currInv = -1; + %this.currInvSlot = -1; + + %player.unmountImage(0); + %player.playThread(1, "root"); +} diff --git a/scripts/server/modes.cs b/scripts/server/modes.cs new file mode 100644 index 0000000..a306116 --- /dev/null +++ b/scripts/server/modes.cs @@ -0,0 +1,497 @@ +// Handles the duplicator state machine. Does not validate transitions! +// ------------------------------------------------------------------- + +//Base class for all duplicator modes +/////////////////////////////////////////////////////////////////////////// + +function NewDuplicatorMode::onStartMode(%this, %client, %lastMode){} +function NewDuplicatorMode::onChangeMode(%this, %client, %nextMode){} +function NewDuplicatorMode::onKillMode(%this, %client){} + +function NewDuplicatorMode::onSelectObject(%this, %client, %obj, %pos, %normal){} + +function NewDuplicatorMode::onLight(%this, %client){} +function NewDuplicatorMode::onNextSeat(%this, %client){} +function NewDuplicatorMode::onPrevSeat(%this, %client){} +function NewDuplicatorMode::onShiftBrick(%this, %client, %x, %y, %z){} +function NewDuplicatorMode::onSuperShiftBrick(%this, %client, %x, %y, %z){} +function NewDuplicatorMode::onRotateBrick(%this, %client, %direction){} +function NewDuplicatorMode::onPlantBrick(%this, %client){} +function NewDuplicatorMode::onCancelBrick(%this, %client){} + +function NewDuplicatorMode::onCopy(%this, %client) +{ + messageClient(%client, '', "\c6Copy can not be used in your current duplicator mode."); +} + +function NewDuplicatorMode::onPaste(%this, %client) +{ + messageClient(%client, '', "\c6Paste can not be used in your current duplicator mode."); +} + +function NewDuplicatorMode::onCut(%this, %client) +{ + messageClient(%client, '', "\c6Cut can not be used in your current duplicator mode."); +} + +function NewDuplicatorMode::getBottomPrint(%this, %client){} + + + +//Registering duplicator modes +/////////////////////////////////////////////////////////////////////////// + +//Possible mode indices +$NDM::Disabled = 0; +$NDM::BoxSelect = 1; +$NDM::BoxSelectProgress = 2; +$NDM::CutProgress = 3; +$NDM::FillColor = 4; +$NDM::FillColorProgress = 5; +$NDM::StackSelect = 6; +$NDM::StackSelectProgress = 7; +$NDM::PlantCopy = 8; +$NDM::PlantCopyProgress = 9; +$NDM::WrenchProgress = 10; +$NDM::SaveProgress = 11; +$NDM::LoadProgress = 12; +$NDM::SuperCutProgress = 13; + +//Create all the pseudo-classes to handle callbacks +function ndRegisterDuplicatorModes() +{ + echo("ND: Registering duplicator modes"); + + //Disabled duplicator mode (does nothing) + ND_ServerGroup.add( + new ScriptObject(NDM_Disabled) + { + class = "NewDuplicatorMode"; + index = $NDM::Disabled; + image = "ND_Image"; + spin = false; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Box Select duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_BoxSelect) + { + class = "NewDuplicatorMode"; + index = $NDM::BoxSelect; + image = "ND_Image_Box"; + spin = false; + + allowSelecting = true; + allowUnMount = false; + } + ); + + //Box Select Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_BoxSelectProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::BoxSelectProgress; + image = "ND_Image_Box"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Cut Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_CutProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::CutProgress; + image = "any"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Fill Color duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_FillColor) + { + class = "NewDuplicatorMode"; + index = $NDM::FillColor; + image = "any"; + spin = false; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Fill Color Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_FillColorProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::FillColorProgress; + image = "any"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Plant Copy duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_PlantCopy) + { + class = "NewDuplicatorMode"; + index = $NDM::PlantCopy; + image = "ND_Image_Blue"; + spin = false; + + allowSelecting = true; + allowUnMount = true; + } + ); + + //Plant Copy Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_PlantCopyProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::PlantCopyProgress; + image = "ND_Image_Blue"; + spin = true; + + allowSelecting = false; + allowUnMount = true; + } + ); + + //Stack Select duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_StackSelect) + { + class = "NewDuplicatorMode"; + index = $NDM::StackSelect; + image = "ND_Image"; + spin = false; + + allowSelecting = true; + allowUnMount = false; + } + ); + + //Stack Select Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_StackSelectProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::StackSelectProgress; + image = "ND_Image"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Wrench Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_WrenchProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::WrenchProgress; + image = "any"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Save Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_SaveProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::SaveProgress; + image = "any"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Load Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_LoadProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::LoadProgress; + image = "any"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //Supercut Progress duplicator mode + ND_ServerGroup.add( + new ScriptObject(NDM_SuperCutProgress) + { + class = "NewDuplicatorMode"; + index = $NDM::SuperCutProgress; + image = "ND_Image_Box"; + spin = true; + + allowSelecting = false; + allowUnMount = false; + } + ); + + //If clients already exist, reset their modes + for(%i = 0; %i < ClientGroup.getCount(); %i++) + { + %cl = ClientGroup.getObject(%i); + + %cl.ndPivot = true; + %cl.ndLimited = true; + %cl.ndDirection = true; + %cl.ndForcePlant = false; + + %cl.ndImage = ND_Image.getId(); + %cl.ndMode = NDM_Disabled; + %cl.ndModeIndex = $NDM::Disabled; + %cl.ndLastSelectMode = NDM_StackSelect; + } +} + + + +//Switching modes +/////////////////////////////////////////////////////////////////////////// + +//Change duplication mode +function GameConnection::ndSetMode(%this, %newMode) +{ + %oldMode = %this.ndMode; + + if(%oldMode.index == %newMode.index) + return; + + %this.ndMode = %newMode; + %this.ndModeIndex = %newMode.index; + + %oldMode.onChangeMode(%this, %newMode.index); + %newMode.onStartMode(%this, %oldMode.index); + + //Enable keybinds + if(!%oldMode.index) + { + commandToClient(%this, 'ndEnableKeybinds', true); + %client.ndMultiselect = false; + } + + //Change image + if(%newMode.image !$= "any") + %this.ndSetImage(nameToId(%newMode.image)); + + //Start or stop spinning + %this.player.setImageLoaded(0, !%newMode.spin); +} + +//Kill duplication mode +function GameConnection::ndKillMode(%this) +{ + if(!%this.ndModeIndex) + return; + + %this.ndMode.onKillMode(%this); + + %this.ndMode = NDM_Disabled; + %this.ndModeIndex = $NDM::Disabled; + + %this.ndUpdateBottomPrint(); + + //Disable keybinds + commandToClient(%this, 'ndEnableKeybinds', false); +} + + + +//Bottomprints +/////////////////////////////////////////////////////////////////////////// + +//Update the bottomprint +function GameConnection::ndUpdateBottomPrint(%this) +{ + if(%this.ndModeIndex) + commandToClient(%this, 'bottomPrint', %this.ndMode.getBottomPrint(%this), 0, true); + else + commandToClient(%this, 'clearBottomPrint'); +} + +//Format bottomprint message with left and right justified text +function ndFormatMessage(%title, %l0, %r0, %l1, %r1, %l2, %r2) +{ + %message = ""; + + //Last used alignment, false = left | true = right + %align = false; + + if(strStr("\c0\c1\c2\c3\c4\c5\c6\c7\c8\c9", getSubStr(%title, 0, 1)) < 0) + %message = %message @ "\c6"; + + %message = %message @ %title @ "\n"; + + for(%i = 0; strLen(%l[%i]) || strLen(%r[%i]); %i++) + { + if(strLen(%l[%i])) + { + if(%align) + %message = %message @ ""; + + if(strStr("\c0\c1\c2\c3\c4\c5\c6\c7\c8\c9", getSubStr(%l[%i], 0, 1)) < 0) + %message = %message @ "\c6"; + + %message = %message @ %l[%i]; + %align = false; + } + + if(strLen(%r[%i])) + { + if(!%align) + %message = %message @ ""; + + if(strStr("\c0\c1\c2\c3\c4\c5\c6\c7\c8\c9", getSubStr(%r[%i], 0, 1)) < 0) + %message = %message @ "\c6"; + + %message = %message @ %r[%i] @ " "; + %align = true; + } + + %message = %message @ "\n"; + } + + return %message @ " "; +} + + + +//Connecting, disconnecting, death +/////////////////////////////////////////////////////////////////////////// + +package NewDuplicator_Server +{ + //Set initial variables on join + function GameConnection::onClientEnterGame(%this) + { + %this.ndPivot = true; + %this.ndLimited = true; + %this.ndDirection = true; + %this.ndForcePlant = false; + + %this.ndImage = ND_Image.getId(); + %this.ndMode = NDM_Disabled; + %this.ndModeIndex = $NDM::Disabled; + %this.ndLastSelectMode = NDM_StackSelect; + + parent::onClientEnterGame(%this); + } + + //Kill duplicator mode when a client leaves + function GameConnection::onClientLeaveGame(%this) + { + if(%this.ndModeIndex) + %this.ndKillMode(%this); + + %this.ndEquipped = false; + + //Remove from client lists of selections + for(%i = 0; %i < ND_ServerGroup.getCount(); %i++) + { + %obj = ND_ServerGroup.getObject(%i); + + if(%obj.getName() $= "ND_Selection") + { + for(%j = 0; %j < %obj.numClients; %j++) + { + if($NS[%obj, "CL", %j] == %this.getId()) + { + for(%k = %j; %k < (%obj.numClients - 1); %k++) + $NS[%obj, "CL", %k] = $NS[%obj, "CL", %k + 1]; + + %obj.numClients--; + break; + } + } + } + } + + //Delete undo groups + deleteVariables("$NU" @ %this @ "_*"); + + %stack = %this.undoStack; + %max = %stack.head; + + if(%max < %stack.tail) + %max += %stack.size; + + for(%i = %stack.tail; %i < %max; %i++) + { + %val = %stack.val[%i % %stack.size]; + + if(getFieldCount(%val) == 2) + { + %str = getField(%val, 1); + + if( + %str $= "ND_PLANT" + || %str $= "ND_PAINT" + || %str $= "ND_WRENCH" + ){ + %group = getField(%val, 0); + + if(isObject(%group)) + { + %group.brickCount = 0; + %group.delete(); + } + } + } + } + + parent::onClientLeaveGame(%this); + } + + //Kill duplicator mode when a player dies + function GameConnection::onDeath(%this, %a, %b, %c, %d) + { + if(%this.ndModeIndex) + %this.ndKillMode(%this); + + %this.ndEquipped = false; + + parent::onDeath(%this, %a, %b, %c, %d); + } + + //Kill duplicator mode when a player is force respawned + function GameConnection::spawnPlayer(%this) + { + if(%this.ndModeIndex) + %this.ndKillMode(%this); + + %this.ndEquipped = false; + + parent::spawnPlayer(%this); + } +}; diff --git a/scripts/server/namedtargets.cs b/scripts/server/namedtargets.cs new file mode 100644 index 0000000..79f0cfc --- /dev/null +++ b/scripts/server/namedtargets.cs @@ -0,0 +1,90 @@ +// Improved versions of setNTObjectName and clearNTObjectName with much +// higher performance. Required to fix lag when clearing named bricks. +// ------------------------------------------------------------------- + +function SimObject::setNTObjectName(%this, %name) +{ + %this = %this.getId(); + %name = getSafeVariableName(trim(%name)); + + if(%name $= "") + { + %this.clearNTObjectName(); + %this.setName(""); + return; + } + + //Names must start with a _ to prevent overwriting real objects + if(getSubStr(%name, 0, 1) !$= "_") + %name = "_" @ %name; + + if(%this.getName() $= %name) + return; + + if(isObject(%name) && !(%name.getType() & $TypeMasks::FxBrickAlwaysObjectType)) + { + error("ERROR: SimObject::setNTObjectName() - Non-Brick object named \"" @ %name @ "\" already exists!"); + return; + } + + %this.clearNTObjectName(); + + %group = %this.getGroup(); + %count = %group.NTObjectCount[%name] | 0; + + if(!%count) + %group.addNTName(%name); + + //Add a reverse lookup to remove the name much faster + %group.NTObject[%name, %count] = %this; + %group.NTObjectIndex[%name, %this] = %count; + %group.NTObjectCount[%name] = %count + 1; + + %this.setName(%name); +} + +function SimObject::clearNTObjectName(%this) +{ + %this = %this.getId(); + %group = %this.getGroup(); + + if(!isObject(%group)) + return; + + %oldName = %this.getName(); + + if(%oldName $= "") + return; + + %index = %group.NTObjectIndex[%oldName, %this]; + %count = %group.NTObjectCount[%oldName]; + + if(%group.NTObject[%oldName, %index] == %this) + { + //Reverse lookup works, use fast version + %lastObj = %group.NTObject[%oldName, %count - 1]; + %group.NTObject[%oldName, %index] = %lastObj; + %group.NTObject[%oldName, %count - 1] = ""; + %group.NTObjectIndex[%oldName, %lastObj] = %index; + %group.NTObjectIndex[%oldName, %this] = ""; + %group.NTObjectCount[%oldName]--; + } + else + { + //Reverse lookup failed, use old and slow version + for(%i = 0; %i < %count; %i++) + { + if(%group.NTObject[%oldName, %i] == %this) + { + %group.NTObject[%oldName, %i] = %group.NTObject[%oldName, %count - 1]; + %group.NTObject[%oldName, %count - 1] = ""; + %group.NTObjectCount[%oldName]--; + break; + } + } + } + + if(!%group.NTObjectCount[%oldName]) + %group.removeNTName(%oldName); +} + diff --git a/scripts/server/prefs.cs b/scripts/server/prefs.cs new file mode 100644 index 0000000..eadb0e2 --- /dev/null +++ b/scripts/server/prefs.cs @@ -0,0 +1,212 @@ +// Detects common services like RTB and registers perferences to them. +// ------------------------------------------------------------------- + +function ndRegisterPrefs() +{ + //Glass prefs also set this variable so we don't need to add them seperately + if($RTB::Hooks::ServerControl) + ndRegisterPrefsToRtb(); + else + ndExtendDefaultPrefValues(); + + ndDeleteOutdatedPrefs(); +} + +function ndRegisterPrefsToRtb() +{ + echo("ND: Registering RTB prefs"); + %trustDropDown = "list None 0 Build 1 Full 2 Self 3"; + + //Limits + RTB_registerPref("Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::AdminOnly", "bool", "Tool_NewDuplicator", false, false, false, ""); + RTB_registerPref("Fill Paint Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::PaintAdminOnly", "bool", "Tool_NewDuplicator", false, false, false, ""); + RTB_registerPref("Fill Paint Fx Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::PaintFxAdminOnly", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_registerPref("Fill Wrench Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::WrenchAdminOnly", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_registerPref("Floating Bricks Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::FloatAdminOnly", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_registerPref("Save Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::SaveAdminOnly", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_registerPref("Load Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::LoadAdminOnly", "bool", "Tool_NewDuplicator", false, false, false, ""); + RTB_registerPref("Fill Bricks Admin Only", "New Duplicator | Limits", "$Pref::Server::ND::FillBricksAdminOnly", "bool", "Tool_NewDuplicator", true, false, false, ""); + + //Settings + RTB_RegisterPref("Trust Limit", "New Duplicator | Settings", "$Pref::Server::ND::TrustLimit", %trustDropDown, "Tool_NewDuplicator", 2, false, false, ""); + RTB_RegisterPref("Admin Trust Bypass (Select)", "New Duplicator | Settings", "$Pref::Server::ND::AdminTrustBypass1", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_RegisterPref("Admin Trust Bypass (Edit)", "New Duplicator | Settings", "$Pref::Server::ND::AdminTrustBypass2", "bool", "Tool_NewDuplicator", false, false, false, ""); + RTB_RegisterPref("Select Public Bricks", "New Duplicator | Settings", "$Pref::Server::ND::SelectPublicBricks", "bool", "Tool_NewDuplicator", true, false, false, ""); + + RTB_registerPref("Max Bricks (Admin)", "New Duplicator | Settings", "$Pref::Server::ND::MaxBricksAdmin", "int 1000 1000000", "Tool_NewDuplicator", 1000000, false, false, ""); + RTB_registerPref("Max Bricks (Player)", "New Duplicator | Settings", "$Pref::Server::ND::MaxBricksPlayer", "int 1000 1000000", "Tool_NewDuplicator", 50000, false, false, ""); + RTB_registerPref("Max Box Size (Admin)", "New Duplicator | Settings", "$Pref::Server::ND::MaxBoxSizeAdmin", "int 1 50000", "Tool_NewDuplicator", 1024, false, false, ""); + RTB_registerPref("Max Box Size (Player)", "New Duplicator | Settings", "$Pref::Server::ND::MaxBoxSizePlayer", "int 1 50000", "Tool_NewDuplicator", 64, false, false, ""); + + RTB_registerPref("Selecting Timeout (Player)", "New Duplicator | Settings", "$Pref::Server::ND::SelectTimeoutMS", "int 0 5000", "Tool_NewDuplicator", 400, false, false, ""); + RTB_registerPref("Planting Timeout (Player)", "New Duplicator | Settings", "$Pref::Server::ND::PlantTimeoutMS", "int 0 5000", "Tool_NewDuplicator", 400, false, false, ""); + + //Advanced + RTB_registerPref("Enable Menu Sounds", "New Duplicator | Advanced", "$Pref::Server::ND::PlayMenuSounds", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_registerPref("Max Ghost Bricks", "New Duplicator | Advanced", "$Pref::Server::ND::MaxGhostBricks", "int 1 50000", "Tool_NewDuplicator", 1500, false, false, ""); + RTB_registerPref("Instant Ghost Bricks", "New Duplicator | Advanced", "$Pref::Server::ND::InstantGhostBricks", "int 1 50000", "Tool_NewDuplicator", 150, false, false, ""); + RTB_registerPref("Scatter Ghost Bricks", "New Duplicator | Advanced", "$Pref::Server::ND::ScatterGhostBricks", "bool", "Tool_NewDuplicator", true, false, false, ""); + RTB_registerPref("Process Bricks per Tick", "New Duplicator | Advanced", "$Pref::Server::ND::ProcessPerTick", "int 1 50000", "Tool_NewDuplicator", 300, false, false, ""); + RTB_registerPref("Box Selection Chunk Size", "New Duplicator | Advanced", "$Pref::Server::ND::BoxSelectChunkDim", "int 1 50000", "Tool_NewDuplicator", 6, false, false, ""); + RTB_registerPref("Create Sym Table on Start", "New Duplicator | Advanced", "$Pref::Server::ND::SymTableOnStart", "bool", "Tool_NewDuplicator", false, false, false, ""); + + //Restore default prefs + RTB_registerPref("Check to restore defaults", "New Duplicator | Reset Prefs", "$ND::RestoreDefaultPrefs", "bool", "Tool_NewDuplicator", false, false, false, "ndRestoreDefaultPrefs"); +} + +//Callback function for "Reset Prefs" +function ndRestoreDefaultPrefs() +{ + if($ND::RestoreDefaultPrefs) + ndApplyDefaultPrefValues(); +} + +function ndExtendDefaultPrefValues() +{ + echo("ND: Extending default pref values"); + + //Limits + if($Pref::Server::ND::AdminOnly $= "") $Pref::Server::ND::AdminOnly = false; + if($Pref::Server::ND::PaintAdminOnly $= "") $Pref::Server::ND::PaintAdminOnly = false; + if($Pref::Server::ND::PaintFxAdminOnly $= "") $Pref::Server::ND::PaintFxAdminOnly = true; + if($Pref::Server::ND::WrenchAdminOnly $= "") $Pref::Server::ND::WrenchAdminOnly = true; + if($Pref::Server::ND::FloatAdminOnly $= "") $Pref::Server::ND::FloatAdminOnly = true; + if($Pref::Server::ND::SaveAdminOnly $= "") $Pref::Server::ND::SaveAdminOnly = true; + if($Pref::Server::ND::LoadAdminOnly $= "") $Pref::Server::ND::LoadAdminOnly = false; + if($Pref::Server::ND::FillBricksAdminOnly $= "") $Pref::Server::ND::FillBricksAdminOnly = true; + + //Settings + if($Pref::Server::ND::TrustLimit $= "") $Pref::Server::ND::TrustLimit = 2; + if($Pref::Server::ND::AdminTrustBypass1 $= "") $Pref::Server::ND::AdminTrustBypass1 = true; + if($Pref::Server::ND::AdminTrustBypass2 $= "") $Pref::Server::ND::AdminTrustBypass2 = false; + if($Pref::Server::ND::SelectPublicBricks $= "") $Pref::Server::ND::SelectPublicBricks = true; + + if($Pref::Server::ND::MaxBricksAdmin $= "") $Pref::Server::ND::MaxBricksAdmin = 1000000; + if($Pref::Server::ND::MaxBricksPlayer $= "") $Pref::Server::ND::MaxBricksPlayer = 10000; + if($Pref::Server::ND::MaxBoxSizeAdmin $= "") $Pref::Server::ND::MaxBoxSizeAdmin = 1024; + if($Pref::Server::ND::MaxBoxSizePlayer $= "") $Pref::Server::ND::MaxBoxSizePlayer = 64; + + if($Pref::Server::ND::SelectTimeoutMS $= "") $Pref::Server::ND::SelectTimeoutMS = 400; + if($Pref::Server::ND::PlantTimeoutMS $= "") $Pref::Server::ND::PlantTimeoutMS = 400; + + //Advanced + if($Pref::Server::ND::PlayMenuSounds $= "") $Pref::Server::ND::PlayMenuSounds = true; + if($Pref::Server::ND::MaxGhostBricks $= "") $Pref::Server::ND::MaxGhostBricks = 1500; + if($Pref::Server::ND::InstantGhostBricks $= "") $Pref::Server::ND::InstantGhostBricks = 150; + if($Pref::Server::ND::ScatterGhostBricks $= "") $Pref::Server::ND::ScatterGhostBricks = true; + if($Pref::Server::ND::ProcessPerTick $= "") $Pref::Server::ND::ProcessPerTick = 300; + if($Pref::Server::ND::BoxSelectChunkDim $= "") $Pref::Server::ND::BoxSelectChunkDim = 6; + if($Pref::Server::ND::SymTableOnStart $= "") $Pref::Server::ND::SymTableOnStart = false; + + //Always set this to false so we don't accidently reset the prefs + $ND::RestoreDefaultPrefs = false; +} + +function ndApplyDefaultPrefValues() +{ + echo("ND: Applying default pref values"); + messageAll('', "\c6(\c3New Duplicator\c6) \c6Prefs reset to default values."); + + //Limits + $Pref::Server::ND::AdminOnly = false; + $Pref::Server::ND::PaintAdminOnly = false; + $Pref::Server::ND::PaintFxAdminOnly = true; + $Pref::Server::ND::WrenchAdminOnly = true; + $Pref::Server::ND::FloatAdminOnly = true; + $Pref::Server::ND::SaveAdminOnly = true; + $Pref::Server::ND::LoadAdminOnly = false; + $Pref::Server::ND::FillBricksAdminOnly = true; + + //Settings + $Pref::Server::ND::TrustLimit = 2; + $Pref::Server::ND::AdminTrustBypass1 = true; + $Pref::Server::ND::AdminTrustBypass2 = false; + $Pref::Server::ND::SelectPublicBricks = true; + + $Pref::Server::ND::MaxBricksAdmin = 1000000; + $Pref::Server::ND::MaxBricksPlayer = 10000; + $Pref::Server::ND::MaxBoxSizeAdmin = 1024; + $Pref::Server::ND::MaxBoxSizePlayer = 64; + + $Pref::Server::ND::SelectTimeoutMS = 400; + $Pref::Server::ND::PlantTimeoutMS = 400; + + //Advanced + $Pref::Server::ND::PlayMenuSounds = true; + $Pref::Server::ND::MaxGhostBricks = 1500; + $Pref::Server::ND::InstantGhostBricks = 150; + $Pref::Server::ND::ScatterGhostBricks = true; + $Pref::Server::ND::ProcessPerTick = 300; + $Pref::Server::ND::BoxSelectChunkDim = 6; + $Pref::Server::ND::SymTableOnStart = false; + + //Always set this to false so we don't accidently reset the prefs + $ND::RestoreDefaultPrefs = false; +} + +//Erases outdated prefs from the config file +function ndDeleteOutdatedPrefs() +{ + //Step 1: Copy all current prefs + //Limits + %adminOnly = $Pref::Server::ND::AdminOnly; + %paintAdminOnly = $Pref::Server::ND::PaintAdminOnly; + %paintFxAdminOnly = $Pref::Server::ND::PaintFxAdminOnly; + %wrenchAdminOnly = $Pref::Server::ND::WrenchAdminOnly; + %floatAdminOnly = $Pref::Server::ND::FloatAdminOnly; + %saveAdminOnly = $Pref::Server::ND::SaveAdminOnly; + %loadAdminOnly = $Pref::Server::ND::LoadAdminOnly; + %fillBricksAdminOnly = $Pref::Server::ND::FillBricksAdminOnly; + //Settings + %trustLimit = $Pref::Server::ND::TrustLimit; + %adminTrustBypass1 = $Pref::Server::ND::AdminTrustBypass1; + %adminTrustBypass2 = $Pref::Server::ND::AdminTrustBypass2; + %selectPublicBricks = $Pref::Server::ND::SelectPublicBricks; + %maxBricksAdmin = $Pref::Server::ND::MaxBricksAdmin; + %maxBricksPlayer = $Pref::Server::ND::MaxBricksPlayer; + %maxBoxSizeAdmin = $Pref::Server::ND::MaxBoxSizeAdmin; + %maxBoxSizePlayer = $Pref::Server::ND::MaxBoxSizePlayer; + %selectTimeoutMS = $Pref::Server::ND::SelectTimeoutMS; + %plantTimeoutMS = $Pref::Server::ND::PlantTimeoutMS; + //Advanced + %playMenuSounds = $Pref::Server::ND::PlayMenuSounds; + %maxGhostBricks = $Pref::Server::ND::MaxGhostBricks; + %instantGhostBricks = $Pref::Server::ND::InstantGhostBricks; + %scatterGhostBricks = $Pref::Server::ND::ScatterGhostBricks; + %processPerTick = $Pref::Server::ND::ProcessPerTick; + %boxSelectChunkDim = $Pref::Server::ND::BoxSelectChunkDim; + %symTableOnStart = $Pref::Server::ND::SymTableOnStart; + + //Step 2: Delete everything + deleteVariables("$Pref::Server::ND::*"); + + //Step 3: Set current prefs again + //Limits + $Pref::Server::ND::AdminOnly = %adminOnly; + $Pref::Server::ND::PaintAdminOnly = %paintAdminOnly; + $Pref::Server::ND::PaintFxAdminOnly = %paintFxAdminOnly; + $Pref::Server::ND::WrenchAdminOnly = %wrenchAdminOnly; + $Pref::Server::ND::FloatAdminOnly = %floatAdminOnly; + $Pref::Server::ND::SaveAdminOnly = %saveAdminOnly; + $Pref::Server::ND::LoadAdminOnly = %loadAdminOnly; + $Pref::Server::ND::FillBricksAdminOnly = %fillBricksAdminOnl; + //Settings + $Pref::Server::ND::TrustLimit = %trustLimit; + $Pref::Server::ND::AdminTrustBypass1 = %adminTrustBypass1; + $Pref::Server::ND::AdminTrustBypass2 = %adminTrustBypass2; + $Pref::Server::ND::SelectPublicBricks = %selectPublicBricks; + $Pref::Server::ND::MaxBricksAdmin = %maxBricksAdmin; + $Pref::Server::ND::MaxBricksPlayer = %maxBricksPlayer; + $Pref::Server::ND::MaxBoxSizeAdmin = %maxBoxSizeAdmin; + $Pref::Server::ND::MaxBoxSizePlayer = %maxBoxSizePlayer; + $Pref::Server::ND::SelectTimeoutMS = %selectTimeoutMS; + $Pref::Server::ND::PlantTimeoutMS = %plantTimeoutMS; + //Advanced + $Pref::Server::ND::PlayMenuSounds = %playMenuSounds; + $Pref::Server::ND::MaxGhostBricks = %maxGhostBricks; + $Pref::Server::ND::InstantGhostBricks = %instantGhostBricks; + $Pref::Server::ND::ScatterGhostBricks = %scatterGhostBricks; + $Pref::Server::ND::ProcessPerTick = %processPerTick; + $Pref::Server::ND::BoxSelectChunkDim = %boxSelectChunkDim; + $Pref::Server::ND::SymTableOnStart = %symTableOnStart; +} diff --git a/scripts/server/symmetrydefinitions.cs b/scripts/server/symmetrydefinitions.cs new file mode 100644 index 0000000..9b19e8d --- /dev/null +++ b/scripts/server/symmetrydefinitions.cs @@ -0,0 +1,180 @@ +// Manually sets up symmetry planes for certain bricks with bad geometry. +// ------------------------------------------------------------------- + +//Manual symmetry can be set using the following variables: +// $ND::ManualSymmetry[UIName] = {0 - 5} +// $ND::ManualSymmetryDB[UIName] = Other UIName +// $ND::ManualSymmetryOffset[UIName] = {0 - 3} + +// $ND::ManualSymmetryZ[UIName] = {true, false} +// $ND::ManualSymmetryZDB[UIName] = Other UIName +// $ND::ManualSymmetryZOffset[UIName] = {0 - 3} + +//Built-in Bricks +$ND::ManualSymmetryZ["1x1 Round"] = true; +$ND::ManualSymmetryZ["1x1F Round"] = true; +$ND::ManualSymmetryZ["Castle Wall"] = true; +$ND::ManualSymmetryZ["1x4x5 Window"] = true; + +//Brick_V15 +$ND::ManualSymmetry["1x4x2 Bars"] = 1; +$ND::ManualSymmetryZ["1x4x2 Bars"] = true; + +//Brick_Treasure_Chest +$ND::ManualSymmetry["Treasure Chest"] = 2; + +//Brick_Teledoor +$ND::ManualSymmetryZ["Teledoor"] = 1; + +//Brick_Halloween +$ND::ManualSymmetry["Skull Cool Open"] = 2; +$ND::ManualSymmetry["Skull Cool"] = 2; + +$ND::ManualSymmetry["Pumpkin"] = 3; +$ND::ManualSymmetry["Pumpkin_Face"] = 3; +$ND::ManualSymmetry["Pumpkin_Scared"] = 3; +$ND::ManualSymmetry["Pumpkin_Ascii"] = 3; + +//Brick_PoleAdapters +$ND::ManualSymmetry["1x1x3 Pole"] = 1; +$ND::ManualSymmetry["1x1 Pole"] = 1; +$ND::ManualSymmetry["1x1F Pole"] = 1; + +$ND::ManualSymmetry["1x1F Pole Plus"] = 2; +$ND::ManualSymmetry["1x1F Pole Corner"] = 5; +$ND::ManualSymmetry["1x1F Pole Corner up"] = 2; +$ND::ManualSymmetry["1x1F Pole Corner down"] = 2; +$ND::ManualSymmetry["1x1F Pole T"] = 5; +$ND::ManualSymmetry["1x1F Pole T up"] = 2; +$ND::ManualSymmetry["1x1F Pole T down"] = 2; +$ND::ManualSymmetry["1x1F Pole X Vert"] = 2; +$ND::ManualSymmetry["1x1F Pole X"] = 1; + +$ND::ManualSymmetryZ["1x1F Pole Plus"] = true; +$ND::ManualSymmetryZ["1x1F Pole Corner"] = true; +$ND::ManualSymmetryZ["1x1F Pole Corner up"] = false; +$ND::ManualSymmetryZ["1x1F Pole Corner down"] = false; +$ND::ManualSymmetryZ["1x1F Pole T"] = true; +$ND::ManualSymmetryZ["1x1F Pole T up"] = false; +$ND::ManualSymmetryZ["1x1F Pole T down"] = false; +$ND::ManualSymmetryZ["1x1F Pole X Vert"] = true; +$ND::ManualSymmetryZ["1x1F Pole X"] = true; + +$ND::ManualSymmetryZDB["1x1F Pole Corner up"] = "1x1F Pole Corner down"; +$ND::ManualSymmetryZDB["1x1F Pole Corner down"] = "1x1F Pole Corner up"; +$ND::ManualSymmetryZDB["1x1F Pole T up"] = "1x1F Pole T down"; +$ND::ManualSymmetryZDB["1x1F Pole T down"] = "1x1F Pole T up"; + +$ND::ManualSymmetryZOffset["1x1F Pole Corner up"] = 0; +$ND::ManualSymmetryZOffset["1x1F Pole Corner down"] = 0; +$ND::ManualSymmetryZOffset["1x1F Pole T up"] = 0; +$ND::ManualSymmetryZOffset["1x1F Pole T down"] = 0; + +//Brick_PoleDiagonals +$ND::ManualSymmetryZ["1x1f Horiz. Diag."] = true; +$ND::ManualSymmetryZ["2x2f Horiz. Diag."] = true; +$ND::ManualSymmetryZ["3x3f Horiz. Diag."] = true; +$ND::ManualSymmetryZ["4x4f Horiz. Diag."] = true; +$ND::ManualSymmetryZ["5x5f Horiz. Diag."] = true; +$ND::ManualSymmetryZ["6x6f Horiz. Diag."] = true; + +$ND::ManualSymmetry["1x1 Vert. Diag. A"] = 2; +$ND::ManualSymmetry["2x2 Vert. Diag. A"] = 2; +$ND::ManualSymmetry["3x3 Vert. Diag. A"] = 2; +$ND::ManualSymmetry["4x4 Vert. Diag. A"] = 2; +$ND::ManualSymmetry["5x5 Vert. Diag. A"] = 2; +$ND::ManualSymmetry["6x6 Vert. Diag. A"] = 2; +$ND::ManualSymmetry["1x1 Vert. Diag. B"] = 2; +$ND::ManualSymmetry["2x2 Vert. Diag. B"] = 2; +$ND::ManualSymmetry["3x3 Vert. Diag. B"] = 2; +$ND::ManualSymmetry["4x4 Vert. Diag. B"] = 2; +$ND::ManualSymmetry["5x5 Vert. Diag. B"] = 2; +$ND::ManualSymmetry["6x6 Vert. Diag. B"] = 2; + +$ND::ManualSymmetryZ["1x1 Vert. Diag. A"] = false; +$ND::ManualSymmetryZ["2x2 Vert. Diag. A"] = false; +$ND::ManualSymmetryZ["3x3 Vert. Diag. A"] = false; +$ND::ManualSymmetryZ["4x4 Vert. Diag. A"] = false; +$ND::ManualSymmetryZ["5x5 Vert. Diag. A"] = false; +$ND::ManualSymmetryZ["6x6 Vert. Diag. A"] = false; +$ND::ManualSymmetryZ["1x1 Vert. Diag. B"] = false; +$ND::ManualSymmetryZ["2x2 Vert. Diag. B"] = false; +$ND::ManualSymmetryZ["3x3 Vert. Diag. B"] = false; +$ND::ManualSymmetryZ["4x4 Vert. Diag. B"] = false; +$ND::ManualSymmetryZ["5x5 Vert. Diag. B"] = false; +$ND::ManualSymmetryZ["6x6 Vert. Diag. B"] = false; + +$ND::ManualSymmetryZDB["1x1 Vert. Diag. A"] = "1x1 Vert. Diag. B"; +$ND::ManualSymmetryZDB["2x2 Vert. Diag. A"] = "2x2 Vert. Diag. B"; +$ND::ManualSymmetryZDB["3x3 Vert. Diag. A"] = "3x3 Vert. Diag. B"; +$ND::ManualSymmetryZDB["4x4 Vert. Diag. A"] = "4x4 Vert. Diag. B"; +$ND::ManualSymmetryZDB["5x5 Vert. Diag. A"] = "5x5 Vert. Diag. B"; +$ND::ManualSymmetryZDB["6x6 Vert. Diag. A"] = "6x6 Vert. Diag. B"; +$ND::ManualSymmetryZDB["1x1 Vert. Diag. B"] = "1x1 Vert. Diag. A"; +$ND::ManualSymmetryZDB["2x2 Vert. Diag. B"] = "2x2 Vert. Diag. A"; +$ND::ManualSymmetryZDB["3x3 Vert. Diag. B"] = "3x3 Vert. Diag. A"; +$ND::ManualSymmetryZDB["4x4 Vert. Diag. B"] = "4x4 Vert. Diag. A"; +$ND::ManualSymmetryZDB["5x5 Vert. Diag. B"] = "5x5 Vert. Diag. A"; +$ND::ManualSymmetryZDB["6x6 Vert. Diag. B"] = "6x6 Vert. Diag. A"; + +$ND::ManualSymmetryZOffset["1x1 Vert. Diag. A"] = 2; +$ND::ManualSymmetryZOffset["2x2 Vert. Diag. A"] = 2; +$ND::ManualSymmetryZOffset["3x3 Vert. Diag. A"] = 2; +$ND::ManualSymmetryZOffset["4x4 Vert. Diag. A"] = 2; +$ND::ManualSymmetryZOffset["5x5 Vert. Diag. A"] = 2; +$ND::ManualSymmetryZOffset["6x6 Vert. Diag. A"] = 2; +$ND::ManualSymmetryZOffset["1x1 Vert. Diag. B"] = 2; +$ND::ManualSymmetryZOffset["2x2 Vert. Diag. B"] = 2; +$ND::ManualSymmetryZOffset["3x3 Vert. Diag. B"] = 2; +$ND::ManualSymmetryZOffset["4x4 Vert. Diag. B"] = 2; +$ND::ManualSymmetryZOffset["5x5 Vert. Diag. B"] = 2; +$ND::ManualSymmetryZOffset["6x6 Vert. Diag. B"] = 2; + +//Brick_GrillPlate +$ND::ManualSymmetry["Grill Corner"] = 4; + +//Brick_Bevel +$ND::ManualSymmetry["1x1F Beveled"] = 1; +$ND::ManualSymmetry["1x1FF Beveled"] = 1; +$ND::ManualSymmetry["1x2F Beveled"] = 1; +$ND::ManualSymmetry["1x2FF Beveled"] = 1; +$ND::ManualSymmetry["1x1 Beveled"] = 1; +$ND::ManualSymmetry["1x1x2 Beveled"] = 1; +$ND::ManualSymmetry["1x2 Beveled"] = 1; +$ND::ManualSymmetry["2x2F Beveled"] = 1; +$ND::ManualSymmetry["2x4F Beveled"] = 1; +$ND::ManualSymmetry["2x4FF Beveled"] = 1; + +$ND::ManualSymmetryZ["1x1F Beveled"] = true; +$ND::ManualSymmetryZ["1x1FF Beveled"] = true; +$ND::ManualSymmetryZ["1x2F Beveled"] = true; +$ND::ManualSymmetryZ["1x2FF Beveled"] = true; +$ND::ManualSymmetryZ["1x1 Beveled"] = true; +$ND::ManualSymmetryZ["1x1x2 Beveled"] = true; +$ND::ManualSymmetryZ["1x2 Beveled"] = true; +$ND::ManualSymmetryZ["2x2F Beveled"] = true; +$ND::ManualSymmetryZ["2x4F Beveled"] = true; +$ND::ManualSymmetryZ["2x4FF Beveled"] = true; + +//Brick_1RandomPack +$ND::ManualSymmetry["2x2x2 Octo Elbow Horz"] = 4; + +$ND::ManualSymmetryZ["1x1 Octo"] = true; +$ND::ManualSymmetryZ["1x1x2 Octo"] = true; +$ND::ManualSymmetryZ["2x2x2 Octo T Horz"] = true; +$ND::ManualSymmetryZ["2x2x2 Octo Elbow Horz"] = true; + +$ND::ManualSymmetryZ["2x3x2 Octo Offset"] = false; +$ND::ManualSymmetryZDB["2x3x2 Octo Offset"] = "2x3x2 Octo Offset"; +$ND::ManualSymmetryZOffset["2x3x2 Octo Offset"] = 2; + +//Brick_Fence +$ND::ManualSymmetry["1x4 Fence"] = 3; + +//Brick_SmallBricks +$ND::ManualSymmetry["0.25x0.25 Corner"] = 4; +$ND::ManualSymmetry["0.25x0.25F Corner"] = 4; +$ND::ManualSymmetry["0.5x0.5 Corner"] = 4; +$ND::ManualSymmetry["0.5x0.5F Corner"] = 4; +$ND::ManualSymmetry["0.75x0.75 Corner"] = 4; +$ND::ManualSymmetry["0.75x0.75F Corner"] = 4; diff --git a/scripts/server/symmetrytable.cs b/scripts/server/symmetrytable.cs new file mode 100644 index 0000000..1a60dcc --- /dev/null +++ b/scripts/server/symmetrytable.cs @@ -0,0 +1,692 @@ +// Analyzes brick geometry to detect common symmetry planes. +// ------------------------------------------------------------------- + +//Begin generating the symmetry table +function ndCreateSymmetryTable() +{ + if($ND::SymmetryTableCreating) + return; + + //Tell everyone what is happening + messageAll('', "\c6(\c3New Duplicator\c6) \c6Creating brick symmetry table..."); + + //Make sure we have the uiname table for manual symmetry + if(!$UINameTableCreated) + createUINameTable(); + + //Delete previous data + deleteVariables("$ND::Symmetry*"); + + $ND::SymmetryTableCreated = false; + $ND::SymmetryTableCreating = true; + $ND::SymmetryTableStarted = getRealTime(); + + $NDT::SimpleCount = 0; + $NDT::MeshCount = 0; + + $NDT::AsymXCountTotal = 0; + $NDT::AsymZCountTotal = 0; + + echo("ND: Start building brick symmetry table..."); + echo("=========================================================================="); + + ndTickCreateSymmetryTable(0, getDatablockGroupSize()); +} + +//Process symmetry for 6 datablocks each tick +function ndTickCreateSymmetryTable(%lastIndex, %max) +{ + %processed = 0; + %limit = $Server::Dedicated ? 400 : 200; + + for(%i = %lastIndex; %i < %max; %i++) + { + %db = getDatablock(%i); + + if(%db.getClassName() $= "FxDtsBrickData") + { + %processed += ndTestBrickSymmetry(%db); + + if(%processed > %limit) + { + schedule(30, 0, ndTickCreateSymmetryTable, %i + 1, %max); + return; + } + } + } + + %simple = $NDT::SimpleCount; + %mesh = $NDT::MeshCount; + + %asymx = $NDT::AsymXCountTotal; + %asymz = $NDT::AsymZCountTotal; + + echo("=========================================================================="); + echo("ND: Finished basic symmetry tests: " @ %simple @ " simple, " @ %mesh @ " with mesh, " @ %asymx @ " asymmetric, " @ %asymz @ " z-asymmetric"); + + ndFindSymmetricPairs(); +} + +//Attempt to find symmetric pairs between asymmetric bricks +function ndFindSymmetricPairs() +{ + echo("ND: Starting X symmetric pair search..."); + echo("=========================================================================="); + + for(%i = 0; %i < $NDT::AsymXCountTotal; %i++) + { + %index = $NDT::AsymXBrick[%i]; + + if(!$NDT::SkipAsymX[%index]) + ndFindSymmetricPairX(%index); + } + + echo("=========================================================================="); + echo("ND: Finished finding symmetric pairs"); + echo("ND: Starting Z symmetric pair search..."); + echo("=========================================================================="); + + for(%i = 0; %i < $NDT::AsymZCountTotal; %i++) + { + %index = $NDT::AsymZBrick[%i]; + + if(!$NDT::SkipAsymZ[%index]) + ndFindSymmetricPairZ(%index); + } + + //Delete temporary arrays + deleteVariables("$NDT::*"); + + echo("=========================================================================="); + echo("ND: Finished finding Z symmetric pairs"); + echo("ND: Symmetry table complete in " @ (getRealTime() - $ND::SymmetryTableStarted) / 1000 @ " seconds"); + + $ND::SymmetryTableCreated = true; + $ND::SymmetryTableCreating = false; + + //We're done! + %seconds = mFloatLength((getRealTime() - $ND::SymmetryTableStarted) / 1000, 0); + messageAll('', "\c6(\c3New Duplicator\c6) \c6Created brick symmetry table in " @ %seconds @ " seconds."); +} + +//Test symmetry of a single blb file +function ndTestBrickSymmetry(%datablock) +{ + //Open blb file + %file = new FileObject(); + %file.openForRead(%datablock.brickFile); + + //Skip brick size - irrelevant + %file.readLine(); + + //Simple bricks are always fully symmetric + if(%file.readLine() $= "BRICK") + { + $NDT::SimpleCount++; + + $ND::Symmetry[%datablock] = 1; + $ND::SymmetryZ[%datablock] = true; + + %file.close(); + %file.delete(); + return 2; + } + + //Not simple, get mesh data index in temp arrays + %dbi = $NDT::MeshCount; + $NDT::MeshCount++; + + //Load mesh from blb file + %faces = 0; + %points = 0; + + while(!%file.isEOF()) + { + //Find start of face + %line = %file.readLine(); + + if(getSubStr(%line, 0, 4) $= "TEX:") + { + %tex = trim(getSubStr(%line, 4, strLen(%line))); + + //Top and bottom faces have different topology, skip + if(%tex $= "TOP" || %tex $= "BOTTOMLOOP" || %tex $= "BOTTOMEDGE") + continue; + + //Add face + $NDT::FaceTexId[%dbi, %faces] = (%tex $= "SIDE" ? 0 : (%tex $= "RAMP" ? 1 : 2)); + + //Skip useless lines + while(trim(%file.readLine()) !$= "POSITION:") {} + + //Add the 4 points + for(%i = 0; %i < 4; %i++) + { + //Read next line + %line = %file.readLine(); + + //Skip useless blank lines + while(!strLen(%line)) + %line = %file.readLine(); + + //Remove formatting from point + %pos = vectorAdd(%line, "0 0 0"); + + //Round down two digits to fix float errors + %pos = mFloatLength(getWord(%pos, 0), 3) * 1.0 + SPC mFloatLength(getWord(%pos, 1), 3) * 1.0 + SPC mFloatLength(getWord(%pos, 2), 3) * 1.0; + + //Get index of this point + if(!%ptIndex = $NDT::PtAtPosition[%dbi, %pos]) + { + //Points array is 1-indexed so we can quickly test !PtAtPosition[...] + %points++; + %ptIndex = %points; + + //Add new point to array + $NDT::PtPosition[%dbi, %points] = %pos; + $NDT::PtAtPosition[%dbi, %pos] = %points; + } + + //Add face to point + if(!$NDT::PtInFace[%dbi, %faces, %ptIndex]) + { + //Increase first then subtract 1 to get 0 the first time + %fIndex = $NDT::FacesAtPt[%dbi, %ptIndex]++ - 1; + $NDT::FaceAtPt[%dbi, %ptIndex, %fIndex] = %faces; + } + + //Add point to face + $NDT::FacePt[%dbi, %faces, %i] = %ptIndex; + $NDT::PtInFace[%dbi, %faces, %ptIndex] = true; + } + + //Added face + %faces++; + } + } + + $NDT::FaceCount[%dbi] = %faces; + $NDT::Datablock[%dbi] = %datablock; + + %file.close(); + %file.delete(); + + //Possible symmetries: + // 0: asymmetric + // 1: x & y + // 2: x + // 3: y + // 4: x+y + // 5: x-y + + //We will test in the following order: + // X + // Y + // X+Y + // X-Y + // Z + + //Check manual symmetry first + %sym = $ND::ManualSymmetry[%datablock.uiname]; + + if(%sym !$= "") + { + if(!%sym) + { + //Try to find the other brick + %otherdb = $UINameTable[$ND::ManualSymmetryDB[%datablock.uiname]]; + %offset = $ND::ManualSymmetryOffset[%datablock.uiname]; + + //... + if(!isObject(%otherdb)) + { + %otherdb = ""; + %offset = 0; + echo("ND: " @ %datablock.uiname @ " has manual symmetry but the paired brick does not exist"); + } + + $ND::SymmetryXDatablock[%datablock] = %otherdb; + $ND::SymmetryXOffset[%datablock] = %offset; + } + + %manualSym = true; + } + else + { + %failX = ndTestSelfSymmetry(%dbi, 0); + %failY = ndTestSelfSymmetry(%dbi, 1); + + //Diagonals are only needed if the brick isn't symmetric to the axis + if(%failX && %failY) + %failXY = ndTestSelfSymmetry(%dbi, 3); + + //One diagonal is enough, only test second if first one fails + if(%failXY) + %failYX = ndTestSelfSymmetry(%dbi, 4); + + //X, Y symmetry + if(!%failX && !%failY) + %sym = 1; + else if(!%failX) + %sym = 2; + else if(!%failY) + %sym = 3; + else if(!%failXY) + %sym = 4; + else if(!%failYX) + %sym = 5; + else + %sym = 0; + } + + //Check manual symmetry first + %symZ = $ND::ManualSymmetryZ[%datablock.uiname]; + + //Z symmetry + if(%symZ !$= "") + { + if(!%symZ) + { + //Try to find the other brick + %otherdb = $UINameTable[$ND::ManualSymmetryZDB[%datablock.uiname]]; + %offset = $ND::ManualSymmetryZOffset[%datablock.uiname]; + + //... + if(!isObject(%otherdb)) + { + %otherdb = ""; + %offset = 0; + echo("ND: " @ %datablock.uiname @ " has manual Z symmetry but the paired brick does not exist"); + } + + $ND::SymmetryZDatablock[%datablock] = %otherdb; + $ND::SymmetryZOffset[%datablock] = %offset; + } + + %manualZSym = true; + } + else + %symZ = !ndTestSelfSymmetry(%dbi, 2); + + if(!%manualSym && !%sym) + { + //Add to lookup table of X-asymmetric bricks of this type + %bIndex = $NDT::AsymXCount[%faces, %symZ]++; + $NDT::AsymXBrick[%faces, %symZ, %bIndex] = %dbi; + + //Add to list of asymmetric bricks + $NDT::AsymXBrick[$NDT::AsymXCountTotal] = %dbi; + $NDT::AsymXCountTotal++; + } + + if(!%manualZSym && !%symZ) + { + //Add to lookup table of Z-asymmetric bricks of this type + %bIndex = $NDT::AsymZCount[%faces, %sym]++; + $NDT::AsymZBrick[%faces, %sym, %bIndex] = %dbi; + + //Add to list of Z-asymmetric bricks + $NDT::AsymZBrick[$NDT::AsymZCountTotal] = %dbi; + $NDT::AsymZCountTotal++; + } + + //Save symmetries + $ND::Symmetry[%datablock] = %sym; + $ND::SymmetryZ[%datablock] = %symZ; + + //Return processed faces + return %faces; +} + +//Find symmetric pair between two bricks on X axis +function ndFindSymmetricPairX(%dbi) +{ + if($NDT::SkipAsymX[%dbi]) + return; + + %datablock = $NDT::Datablock[%dbi]; + + %zsym = $ND::SymmetryZ[%datablock]; + %faces = $NDT::FaceCount[%dbi]; + %count = $NDT::AsymXCount[%faces, %zsym]; + + //Only potential match is the brick itself - fail + if(%count == 1) + { + echo("ND: No X match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")"); + return; + } + + %off = -1; + $NDT::SkipAsymX[%dbi] = true; + + for(%i = 1; %i <= %count; %i++) + { + %other = $NDT::AsymXBrick[%faces, %zsym, %i]; + + //Don't compare with bricks that already have a pair + if($NDT::SkipAsymX[%other]) + continue; + + //Test all 4 possible rotations + //Not using loop due to lack of goto command + if(!ndTestPairSymmetry(%dbi, %other, true, 0)) + { + %off = 0; + break; + } + + if(!ndTestPairSymmetry(%dbi, %other, true, 1)) + { + %off = 1; + break; + } + + if(!ndTestPairSymmetry(%dbi, %other, true, 2)) + { + %off = 2; + break; + } + + if(!ndTestPairSymmetry(%dbi, %other, true, 3)) + { + %off = 3; + break; + } + } + + if(%off != -1) + { + %otherdb = $NDT::Datablock[%other]; + + //Save symmetry + $ND::SymmetryXDatablock[%datablock] = %otherdb; + $ND::SymmetryXOffset[%datablock] = %off; + + $ND::SymmetryXDatablock[%otherdb] = %datablock; + $ND::SymmetryXOffset[%otherdb] = -%off; + + //No need to process the other brick again + $NDT::SkipAsymX[%other] = true; + } + else + echo("ND: No X match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")"); +} + +//Find symmetric pair between two bricks on Z axis +function ndFindSymmetricPairZ(%dbi) +{ + if($NDT::SkipAsymZ[%dbi]) + return; + + %datablock = $NDT::Datablock[%dbi]; + + %sym = $ND::Symmetry[%datablock]; + %faces = $NDT::FaceCount[%dbi]; + %count = $NDT::AsymZCount[%faces, %sym]; + + //Only potential match is the brick itself - fail + if(%count == 1) + { + echo("ND: No Z match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")"); + return; + } + + %off = -1; + + for(%i = 1; %i <= %count; %i++) + { + %other = $NDT::AsymZBrick[%faces, %sym, %i]; + + //Don't compare with bricks that already have a pair + if($NDT::SkipAsymZ[%other]) + continue; + + //Test all 4 possible rotations + //Not using loop due to lack of goto command + if(!ndTestPairSymmetry(%dbi, %other, false, 0)) + { + %off = 0; + break; + } + + if(!ndTestPairSymmetry(%dbi, %other, false, 1)) + { + %off = 1; + break; + } + + if(!ndTestPairSymmetry(%dbi, %other, false, 2)) + { + %off = 2; + break; + } + + if(!ndTestPairSymmetry(%dbi, %other, false, 3)) + { + %off = 3; + break; + } + } + + //It's possible for a brick to match itself rotated + //here, so only mark it after the search + $NDT::SkipAsymZ[%dbi] = true; + + if(%off != -1) + { + %otherdb = $NDT::Datablock[%other]; + + //Save symmetry + $ND::SymmetryZDatablock[%datablock] = %otherdb; + $ND::SymmetryZOffset[%datablock] = %off; + + $ND::SymmetryZDatablock[%otherdb] = %datablock; + $ND::SymmetryZOffset[%otherdb] = -%off; + + //No need to process the other brick again + $NDT::SkipAsymZ[%other] = true; + } + else + echo("ND: No Z match for " @ %datablock.getName() @ " (" @ %datablock.category @ "/" @ %datablock.subCategory @ "/" @ %datablock.uiname @ ")"); +} + +//Test a mesh for a single symmetry plane in itself +function ndTestSelfSymmetry(%dbi, %plane) +{ + %fail = false; + %faces = $NDT::FaceCount[%dbi]; + + for(%i = 0; %i < %faces; %i++) + { + //If this face was already used by another mirror, skip + if(%skipFace[%i]) + continue; + + //Attempt to find the mirrored points + for(%j = 0; %j < 4; %j++) + { + %pt = $NDT::FacePt[%dbi, %i, %j]; + + //Do we already know the mirrored one? + if(%mirrPt[%pt]) + { + %mirr[%j] = %mirrPt[%pt]; + continue; + } + + //Get position of point + %v = $NDT::PtPosition[%dbi, %pt]; + + //Get point at mirrored position based on plane + switch$(%plane) + { + //Flip X + case 0: %mirr = $NDT::PtAtPosition[%dbi, -firstWord(%v) SPC restWords(%v)]; + //Flip Y + case 1: %mirr = $NDT::PtAtPosition[%dbi, getWord(%v, 0) SPC -getWord(%v, 1) SPC getWord(%v, 2)]; + //Flip Z + case 2: %mirr = $NDT::PtAtPosition[%dbi, getWords(%v, 0, 1) SPC -getWord(%v, 2)]; + //Mirror along X+Y + case 3: %mirr = $NDT::PtAtPosition[%dbi, -getWord(%v, 1) SPC -getWord(%v, 0) SPC getWord(%v, 2)]; + //Mirror along X-Y + default: %mirr = $NDT::PtAtPosition[%dbi, getWord(%v, 1) SPC getWord(%v, 0) SPC getWord(%v, 2)]; + } + + if(%mirr) + { + %mirrPt[%pt] = %mirr; + %mirrPt[%mirr] = %pt; + + %mirr[%j] = %mirr; + } + else + { + %fail = true; + break; + } + } + + if(%fail) + break; + + //Test whether the points have a common face + %fail = true; + %count = $NDT::FacesAtPt[%dbi, %mirr0]; + + for(%j = 0; %j < %count; %j++) + { + %potentialFace = $NDT::FaceAtPt[%dbi, %mirr0, %j]; + + //Mirrored face must have the same texture id + if($NDT::FaceTexId[%dbi, %i] != $NDT::FaceTexId[%dbi, %potentialFace]) + continue; + + //Check whether remaining points are in the face + if(!$NDT::PtInFace[%dbi, %potentialFace, %mirr1]) + continue; + + if(!$NDT::PtInFace[%dbi, %potentialFace, %mirr2]) + continue; + + if(!$NDT::PtInFace[%dbi, %potentialFace, %mirr3]) + continue; + + //We found a matching face! + %skipFace[%potentialFace] = true; + %fail = false; + break; + } + + if(%fail) + break; + } + + return %fail; +} + +//Test X or Z symmetry between two meshes with rotation offset +function ndTestPairSymmetry(%dbi, %other, %plane, %rotation) +{ + %fail = false; + %faces = $NDT::FaceCount[%dbi]; + + for(%i = 0; %i < %faces; %i++) + { + //Attempt to find the mirrored points + for(%j = 0; %j < 4; %j++) + { + %pt = $NDT::FacePt[%dbi, %i, %j]; + + //Do we already know the mirrored one? + if(%mirrPt[%pt]) + { + %mirr[%j] = %mirrPt[%pt]; + continue; + } + + //Get position of point + %v = $NDT::PtPosition[%dbi, %pt]; + + //true = X, false = Z + if(%plane) + { + //Get point at mirrored position based on rotation + switch(%rotation) + { + //Flip X + case 0: %mirr = $NDT::PtAtPosition[%other, -firstWord(%v) SPC restWords(%v)]; + //Flip X, rotate 90 + case 1: %mirr = $NDT::PtAtPosition[%other, getWord(%v, 1) SPC getWord(%v, 0) SPC getWord(%v, 2)]; + //Flip X, rotate 180 + case 2: %mirr = $NDT::PtAtPosition[%other, getWord(%v, 0) SPC -getWord(%v, 1) SPC getWord(%v, 2)]; + //Flip X, rotate 270 + default: %mirr = $NDT::PtAtPosition[%other, -getWord(%v, 1) SPC -getWord(%v, 0) SPC getWord(%v, 2)]; + } + } + else + { + //Get point at mirrored position based on rotation + switch(%rotation) + { + //Flip Z + case 0: %mirr = $NDT::PtAtPosition[%other, getWord(%v, 0) SPC getWord(%v, 1) SPC -getWord(%v, 2)]; + //Flip Z, rotate 90 + case 1: %mirr = $NDT::PtAtPosition[%other, getWord(%v, 1) SPC -getWord(%v, 0) SPC -getWord(%v, 2)]; + //Flip Z, rotate 180 + case 2: %mirr = $NDT::PtAtPosition[%other, -getWord(%v, 0) SPC -getWord(%v, 1) SPC -getWord(%v, 2)]; + //Flip Z, rotate 270 + default: %mirr = $NDT::PtAtPosition[%other, -getWord(%v, 1) SPC getWord(%v, 0) SPC -getWord(%v, 2)]; + } + } + + if(%mirr) + { + %mirrPt[%pt] = %mirr; + %mirr[%j] = %mirr; + } + else + { + %fail = true; + break; + } + } + + if(%fail) + break; + + //Test whether the points have a common face + %fail = true; + %count = $NDT::FacesAtPt[%other, %mirr0]; + + for(%j = 0; %j < %count; %j++) + { + %potentialFace = $NDT::FaceAtPt[%other, %mirr0, %j]; + + //Mirrored face must have the same texture id + if($NDT::FaceTexId[%dbi, %i] != $NDT::FaceTexId[%other, %potentialFace]) + continue; + + //Check whether remaining points are in the face + if(!$NDT::PtInFace[%other, %potentialFace, %mirr1]) + continue; + + if(!$NDT::PtInFace[%other, %potentialFace, %mirr2]) + continue; + + if(!$NDT::PtInFace[%other, %potentialFace, %mirr3]) + continue; + + //We found a matching face! + %fail = false; + break; + } + + if(%fail) + break; + } + + return %fail; +} diff --git a/scripts/server/undo.cs b/scripts/server/undo.cs new file mode 100644 index 0000000..6491c1c --- /dev/null +++ b/scripts/server/undo.cs @@ -0,0 +1,31 @@ +// Handles the undo stack for duplicator actions. +// ------------------------------------------------------------------- + +package NewDuplicator_Server +{ + //Catch things falling off the end of the undo stack + function QueueSO::push(%obj, %val) + { + %lastVal = %obj.val[(%obj.head + 1) % %obj.size]; + + if(getFieldCount(%lastVal) == 2) + { + %str = getField(%lastVal, 1); + + if( + %str $= "ND_PLANT" + || %str $= "ND_PAINT" + || %str $= "ND_WRENCH" + ){ + %qobj = getField(%lastVal, 0); + if(isObject(%qobj)){ + %qobj.delete(); + }else{ + // talk("QueueSO::push(" @ %obj @ ", " @ %val @ ") - Nonexistent object " @ %qobj); + } + } + } + + parent::push(%obj, %val); + } +}; diff --git a/server.cs b/server.cs new file mode 100644 index 0000000..47e0f76 --- /dev/null +++ b/server.cs @@ -0,0 +1,65 @@ +// Executes all required scripts and initializes the server side. +// ------------------------------------------------------------------- + +$ND::Version = "1.6.2"; + +$ND::FilePath = filePath($Con::File) @ "/"; +$ND::ConfigPath = "config/NewDuplicator/"; + +$ND::ClassPath = $ND::FilePath @ "classes/"; +$ND::ScriptPath = $ND::FilePath @ "scripts/"; +$ND::ResourcePath = $ND::FilePath @ "resources/"; + +if(isObject(ND_ServerGroup)) + ND_ServerGroup.delete(); + +new ScriptGroup(ND_ServerGroup); + +exec($ND::ClassPath @ "server/ghostgroup.cs"); +exec($ND::ClassPath @ "server/highlightbox.cs"); +exec($ND::ClassPath @ "server/selection.cs"); +exec($ND::ClassPath @ "server/selectionbox.cs"); +exec($ND::ClassPath @ "server/undogrouppaint.cs"); +exec($ND::ClassPath @ "server/undogroupplant.cs"); +exec($ND::ClassPath @ "server/undogroupwrench.cs"); + +exec($ND::ClassPath @ "server/duplimode/boxselect.cs"); +exec($ND::ClassPath @ "server/duplimode/boxselectprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/cutprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/fillcolor.cs"); +exec($ND::ClassPath @ "server/duplimode/fillcolorprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/loadprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/plantcopy.cs"); +exec($ND::ClassPath @ "server/duplimode/plantcopyprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/saveprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/stackselect.cs"); +exec($ND::ClassPath @ "server/duplimode/stackselectprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/supercutprogress.cs"); +exec($ND::ClassPath @ "server/duplimode/wrenchprogress.cs"); + +exec($ND::ScriptPath @ "common/bytetable.cs"); +exec($ND::ScriptPath @ "server/commands.cs"); +exec($ND::ScriptPath @ "server/datablocks.cs"); +exec($ND::ScriptPath @ "server/functions.cs"); +exec($ND::ScriptPath @ "server/handshake.cs"); +exec($ND::ScriptPath @ "server/highlight.cs"); +exec($ND::ScriptPath @ "server/images.cs"); +exec($ND::ScriptPath @ "server/modes.cs"); +exec($ND::ScriptPath @ "server/namedtargets.cs"); +exec($ND::ScriptPath @ "server/prefs.cs"); +exec($ND::ScriptPath @ "server/symmetrydefinitions.cs"); +exec($ND::ScriptPath @ "server/symmetrytable.cs"); +exec($ND::ScriptPath @ "server/undo.cs"); + +activatePackage(NewDuplicator_Server); +schedule(10, 0, activatePackage, NewDuplicator_Server_Final); + +ndRegisterDuplicatorModes(); +ndRegisterPrefs(); +ndResendHandshakes(); + +if($Pref::Server::ND::SymTableOnStart && !$ND::SymmetryTableCreated){ + schedule(10, 0, ndCreateSymmetryTable); +} + +exec("./v20fix.cs"); diff --git a/v20fix.cs b/v20fix.cs new file mode 100644 index 0000000..b6b648b --- /dev/null +++ b/v20fix.cs @@ -0,0 +1,15 @@ + +function colorFToI(%f){ + %i = %f*255; + %i = mFloor(%i + 0.5); + return %i; +} + +function getColorI(%color){ + %color2 = + colorFToI(getWord(%color, 0)) SPC + colorFToI(getWord(%color, 1)) SPC + colorFToI(getWord(%color, 2)) SPC + colorFToI(getWord(%color, 3)) + ; +}