Vehicles/VehicleScripts/RopeWinch.lua

RopeWinch implements all features required by snowcats that are equipped with winch devices (such as the PistenBully 600 W). These vehicles can then be attached to all anchor points (of type AnchorPoint). Also, there is a VehicleAnchor script which allows for having winch anchor points mounted to vehicles.

Similar to CylinderAnimator, most performance-relevant operations are done by C# code. All RopeWinch functions that are called, but not documented here, are written in C# (e.g. RopeWinch.new or RopeWinch.setIntakeRotation).

  26  RopeWinch                           = RopeWinch or {};
  27  RopeWinch.anchorPoints              = RopeWinch.anchorPoints or {};

RopeWinch.registerAnchorPoint(name, obj)

Register an AnchorPoint so that it can be used by RopeWinch. Note that anchor points are always indexed by their name (a string) as passed to this function!

  32  function RopeWinch.registerAnchorPoint(name, obj)
  33      RopeWinch.anchorPoints[name or ""]  = obj;
  34  end;

RopeWinch.unregisterAnchorPoint(name)

Unregister an AnchorPoint

  38  function RopeWinch.unregisterAnchorPoint(name)
  39      RopeWinch.anchorPoints[name or ""]  = nil;
  40  end;

RopeWinch:load(dataTable)

We need to load some data from our configuration table (dataTable).

First, we set up all custom functions that this script requires. Note that VehicleManager:newFunction() does not allow for return values, therefore we have to link some functions directly. Then, we read in configuration data and apply some defaults.

  47  function RopeWinch:load(dataTable)
  48      self.attachWinchToAnchorPoint   = VehicleManager:newFunction("attachWinchToAnchorPoint");
  49      self.setIsRopeTensed            = VehicleManager:newFunction("setIsRopeTensed");
  50      self.ropeWinchConnectorCallback = VehicleManager:newFunction("ropeWinchConnectorCallback");
  51      self.setWinchRootRotation       = VehicleManager:newFunction("setWinchRootRotation");
  52      
  53      self.getRopeWinchConnectorActive= RopeWinch.getRopeWinchConnectorActive;
  54      self.getMayRopeWinchDisconnect  = RopeWinch.getMayRopeWinchDisconnect;
  55  
  56      if dataTable.ropeWinch ~= nil then
  57          local v                     = dataTable.ropeWinch;
  58          self.winchRoot              = getChild(self.id, v.winchRoot or "");
  59          self.winchIntake            = getChild(self.id, v.winchIntake or "");
  60          local ropeDiameter          = v.ropeDiameter or 0.011;
  61          local ropeWeightPerMeter    = v.weightPerMeter or 45;
  62  
  63          self.ropeWinchNode          = RopeWinch.new(self.id, self.winchRoot, self.winchIntake, ropeDiameter, ropeWeightPerMeter);
  64          self.anchorPointName        = nil;
  65          self.currentSetAnchorPoint  = "";
  66  
  67          if v.intakeDefaultRotation ~= nil then
  68              RopeWinch.setIntakeRotation(self.ropeWinchNode, v.intakeDefaultRotation);
  69          end;
  70          if v.detachedHookIndex ~= nil then
  71              RopeWinch.setDetachedId(self.ropeWinchNode, getChild(self.id, v.detachedHookIndex));
  72          end;
  73          if v.attachedHookIndex ~= nil and v.attachedHookTargetIndex ~= nil then
  74              RopeWinch.setHook(self.ropeWinchNode, getChild(self.id, v.attachedHookIndex), getChild(self.id, v.attachedHookTargetIndex));
  75              self.attachedHookIndex      = v.attachedHookIndex;
  76          end;
  77  
  78          RopeWinch.setIntakeParameters(self.ropeWinchNode,
  79              getNoNil(v.intakeDiameter, 0.35)/2,
  80              getNoNil(v.intakeMinLimit, -80),
  81              getNoNil(v.intakeMaxLimit, 80)
  82          );
  83  
  84  
  85          -- tensioning
  86          self.ropeWinchForceMin      = v.winchForceMin or 1200;
  87          self.ropeWinchForceMax      = v.winchForceMax or 46000;
  88          self.ropeWinchForceSpeed    = 1/math.max(v.forceToggleDuration or 1, 0.1);
  89          self.ropeWinchForceLerp     = 0;
  90          self.ropeWinchForceTarget   = 0;
  91  
  92          -- motorized rotating
  93          self.winchRotateSpeed       = v.winchRotateSpeed or 30;
  94          self.winchRotateAcc         = v.winchRotateAcc or 30;
  95          self.winchCurrentRotSpeed   = 0;
  96  
  97          self.soloForwardStiffness   = v.soloForwardStiffness;
  98          self.soloSidewaysStiffness  = v.soloSidewaysStiffness;
  99  
 100          self.maxForwardStiffness    = v.maxForwardStiffness;
 101          self.maxSidewaysStiffness   = v.maxSidewaysStiffness;
 102  
 103          -- control button for connecting
 104          self.ropeWinchConnector     = getChild(self.id, v.connector or "");
 105          
 106          -- load winch wheels
 107          if v.winchWheels ~= nil then
 108              self.winchWheels        = {};
 109              for kWheel, vWheel in pairs(v.winchWheels) do 
 110                  local wheel         = {
 111                      wheel           = getChild(self.id, vWheel.wheel),
 112                      diameter        = getNoNil(vWheel.diameter, 0),
 113                      reverse         = getNoNil(vWheel.reverse, false),
 114                      axis            = getNoNil(vWheel.axis, 0),
 115                  };
 116                  table.insert(self.winchWheels, wheel);
 117              end;
 118          end;
 119  
 120          -- guidance system
 121          if v.guidanceAnimation ~= nil then
 122              self.guidanceAnimation      = getChild(self.id, v.guidanceAnimation);
 123          end;
 124          if g_scenario.player ~= nil then
 125              self.localPlayerIdentifier  = g_scenario.player;
 126          end;
 127          ControlElement.new(self.ropeWinchConnector);
 128          ControlElement.setCallback(self.ropeWinchConnector, 1,
 129              function()  self:ropeWinchConnectorCallback();          end
 130          );
 131          ControlElement.setActiveCallback(self.ropeWinchConnector,
 132              function()  return self:getRopeWinchConnectorActive();  end
 133          );
 134  
 135          ControlElement.setName(self.ropeWinchConnector, l10n.get("WinchAnchor_Attach"));
 136      end;
 137      self:setIsRopeTensed(false, true);
 138  end;

RopeWinch:destroy()

Destroys the control element again.

 141  function RopeWinch:destroy()
 142      if self.ropeWinchConnector ~= nil then
 143          ControlElement.destroy(self.ropeWinchConnector);
 144      end;
 145  
 146      self:attachWinchToAnchorPoint(nil, true);
 147  end;

RopeWinch:saveToTable(tbl)

All RopeWinch features are correctly saved into the savegame (i.e. winch rotation, active forces and active anchor point).

 151  function RopeWinch:saveToTable(tbl)
 152      if tbl == nil then return end;
 153      if self.ropeWinchNode == nil then return end;
 154  
 155      local rx,ry,rz                  = getRotation(self.winchRoot);
 156  
 157      tbl.ropeWinchForceLerp          = self.ropeWinchForceLerp;
 158      tbl.ropeWinchForceTarget        = self.ropeWinchForceTarget;
 159      tbl.winchRotationY              = ry;
 160  
 161      if self.anchorPointName ~= self.localPlayerIdentifier then
 162          tbl.anchorPointName         = self.anchorPointName;
 163      end;
 164  end;

RopeWinch:loadFromTable(tbl)

Of course, we also need to restore some data from the savegame.

Note that the vehicle will be attached to the anchor point not directly upon loading, but rather during the very next update call.

 170  function RopeWinch:loadFromTable(tbl)
 171      if tbl == nil then return end;
 172      if self.ropeWinchNode == nil then return end;
 173  
 174      if tbl.winchRotationY ~= nil then
 175          self:setWinchRootRotation(tbl.winchRotationY);
 176      end;
 177  
 178      -- return if rope winch is not saved correctly
 179      if tbl.ropeWinchForceLerp == nil then return end;
 180      self.ropeWinchForceLerp         = getNoNil(tbl.ropeWinchForceLerp,      self.ropeWinchForceLerp);
 181      self.ropeWinchForceTarget       = getNoNil(tbl.ropeWinchForceTarget,    self.ropeWinchForceTarget);
 182  
 183      if tbl.anchorPointName ~= nil and tbl.anchorPointName ~= self.localPlayerIdentifier then
 184          -- attach it at the very beginning of the next frame (when loading has already been finished)
 185          self.attachNextFrameTo      = tbl.anchorPointName;
 186      end;
 187  end;

RopeWinch:onReset()

Reset the rope winch as well if the vehicle is resetted. Of course, it also makes sense to disconnect from any anchor point in this case.

 191  function RopeWinch:onReset()
 192      if self.ropeWinchNode == nil then return end;
 193  
 194      self.ropeWinchForceLerp         = 0;
 195      self:setIsRopeTensed(false);
 196  
 197      -- disconnect from anchor
 198      if self.anchorPointName ~= nil then
 199          local anchorPoint           = RopeWinch.anchorPoints[self.anchorPointName or ""];
 200      
 201          if anchorPoint ~= nil then
 202              anchorPoint:disconnect(true);
 203          end;
 204          self:attachWinchToAnchorPoint(nil);
 205      end;
 206  
 207      if AnchorPoint.activeVehicle == self then
 208          AnchorPoint.activeVehicle   = nil;
 209      end;
 210  
 211      self:setWinchRootRotation(0);
 212  end;

RopeWinch:setIsRopeTensed(isTensed, noEvent)

Applies whether the rope is tensed or not, depending on isTensed (bool).

 216  function RopeWinch:setIsRopeTensed(isTensed, noEvent)
 217      if self.ropeWinchNode == nil then return end;
 218  
 219      self.ropeWinchForceTarget       = isTensed and 1 or 0;
 220  
 221      local currentForce              = lerp(self.ropeWinchForceMin, self.ropeWinchForceMax, self.ropeWinchForceLerp);
 222      RopeWinch.setTensioningForce(self.ropeWinchNode, currentForce);
 223      RopeWinch.setSegmentLength(self.ropeWinchNode, lerp(0.5, 2, self.ropeWinchForceLerp));
 224  
 225      -- apply stiffness to the wheels
 226      if self.applyWheelStiffness == nil then return end;
 227  
 228      if isTensed then
 229          if self.maxForwardStiffness ~= nil then
 230              self:applyWheelStiffness(self.maxForwardStiffness, self.maxSidewaysStiffness);
 231          end;
 232      else
 233          if self.soloForwardStiffness ~= nil then
 234              self:applyWheelStiffness(self.soloForwardStiffness, self.soloSidewaysStiffness);
 235          end;
 236      end;
 237  
 238      if not noEvent then
 239          EventSetIsRopeTensed:send(self, isTensed);
 240      end;
 241  end;

RopeWinch:attachWinchToAnchorPoint(anchorPointName, noEvent)

Attach the winch to the anchor point specified in anchorPointName (string). anchorPointName can also be nil to disconnect from any active anchor point.

 245  function RopeWinch:attachWinchToAnchorPoint(anchorPointName, noEvent)
 246      
 247      -- note: this function is ALWAYS called by the anchor point itself
 248      if self.ropeWinchNode == nil then return end;
 249  
 250      if self.anchorPointName ~= nil then
 251          local oldAnchorPoint        = RopeWinch.anchorPoints[self.anchorPointName];
 252  
 253          -- disconnect from there
 254          RopeWinch.setTargetObject(self.ropeWinchNode, 0);
 255          self.anchorPointName        = nil;
 256  
 257          if oldAnchorPoint ~= nil and oldAnchorPoint.connectedVehicle == self then
 258              oldAnchorPoint.connectedVehicle = nil;
 259          end;
 260      end;
 261  
 262      self.anchorPointName            = anchorPointName;
 263      self.currentSetAnchorPoint      = anchorPointName;
 264  
 265      if not noEvent then
 266          EventSetRopeWinchConnection:send(self, anchorPointName);
 267      end;
 268  
 269      if anchorPointName == "" then
 270          self.anchorPointName        = nil;
 271      end;
 272  
 273      if anchorPointName == nil then
 274          -- no attacher point set, so let's loose our rope by force
 275          self:setIsRopeTensed(false, noEvent);
 276      end;
 277  
 278  
 279      local anchorPoint               = RopeWinch.anchorPoints[anchorPointName];
 280  
 281      if anchorPoint ~= nil then
 282          -- there is an anchor point indeed existing, yay
 283          RopeWinch.setTargetObject(self.ropeWinchNode, anchorPoint.ropeTargetId);
 284          anchorPoint.connectedVehicle    = self;
 285      end;
 286  end;

RopeWinch:update(dt)

Reads in some input and passes it on to the C# functions (if necessary) every frame.

 290  function RopeWinch:update(dt)
 291      -- in case the player will by synched after the vehiclePostResync
 292      if self.localPlayerIdentifier == nil then
 293          self.localPlayerIdentifier      = g_scenario.player;
 294      end;
 295  
 296      if self.ropeWinchNode == nil then return end;
 297      
 298      if self.attachNextFrameTo ~= nil then
 299  
 300          local anchorPoint           = RopeWinch.anchorPoints[self.attachNextFrameTo];
 301  
 302          if anchorPoint ~= nil then
 303              -- this will automatically call all functions on our vehicle to attach it there
 304              anchorPoint:attach(self);
 305          end;
 306  
 307          self:setIsRopeTensed(self.ropeWinchForceTarget > 0.5);
 308          self.attachNextFrameTo      = nil;
 309      end;
 310  
 311      -- read winch rope length for wheels
 312      local ropeLength            = RopeWinch.getRopeLength(self.ropeWinchNode) / 2;
 313  
 314      -- update winchWheels if existing
 315      if self.winchWheels ~= nil then
 316          for kWheel, vWheel in pairs(self.winchWheels) do
 317              if vWheel.reverse then
 318                  if vWheel.axis  ==  0 or vWheel.axis ==  1 then
 319                      setRotation(vWheel.wheel, ((ropeLength)/ vWheel.diameter * 360 )/ math.pi, 180, 180);
 320                  elseif vWheel.axis == 2 then
 321                      setRotation(vWheel.wheel, 0, -((ropeLength)/ vWheel.diameter * 360) / math.pi, 0);
 322                  else
 323                      setRotation(vWheel.wheel, 180, 180, ((ropeLength)/ vWheel.diameter * 360) / math.pi);
 324                  end;
 325              else
 326                  if vWheel.axis  ==  0 or vWheel.axis ==  1 then
 327                      setRotation(vWheel.wheel, ((ropeLength)/ vWheel.diameter * 360) / math.pi, 0, 0);
 328                  elseif vWheel.axis == 2 then
 329                      setRotation(vWheel.wheel, 0, ((ropeLength)/ vWheel.diameter * 360) / math.pi, 0);
 330                  else
 331                      setRotation(vWheel.wheel, 0, 0, ((ropeLength)/ vWheel.diameter * 360) / math.pi);
 332                  end;            
 333              end;
 334          end;
 335      end;
 336  
 337      local scope                 = math.pi * 0.946563;
 338      local sliderValue           = Utils.pingPong((ropeLength / (scope*  18)), 1.0);
 339  
 340      if self.guidanceAnimation ~= nil then
 341          Animation.sampleTime(self.guidanceAnimation, sliderValue, "WinchSlider");
 342      end;
 343  
 344      if self.localPlayerIdentifier ~= nil and  self.anchorPointName == self.localPlayerIdentifier and not self.localPlayerIdentifier:getIsLocalPlayerEntered() then
 345          self:attachWinchToAnchorPoint(nil);
 346      end;
 347  
 348      if not self.isActive then return end;
 349  
 350      local rotSpeedTarget            = 0;
 351  
 352      if self.anchorPointName == self.localPlayerIdentifier then
 353          if g_scenario.player ~= nil and not g_scenario.player.isActive then
 354              -- urgently disconnect!
 355              self:attachWinchToAnchorPoint(nil);
 356          end;
 357      end;
 358      
 359      if self:getIsInputActive() then
 360          if self.anchorPointName ~= nil and self.anchorPointName ~= self.localPlayerIdentifier then
 361              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_TenseWinch) then
 362                  self:setIsRopeTensed(self.ropeWinchForceTarget < 0.5);
 363              end;
 364              g_GUI:addKeyHint(InputMapper.VehicleSnowcat_TenseWinch,     l10n.get(self.ropeWinchForceTarget > 0.5    and "Input_VehicleSnowcat_TenseWinch1"  or "Input_VehicleSnowcat_TenseWinch0"));
 365          end;
 366  
 367          if self.anchorPointName == nil then
 368              -- player may now move the winch if he wants to
 369              if InputMapper:getKey(InputMapper.VehicleSnowcat_TurnWinchLeft) then
 370                  -- turn left
 371                  rotSpeedTarget      = -self.winchRotateSpeed;
 372              elseif InputMapper:getKey(InputMapper.VehicleSnowcat_TurnWinchRight) then
 373                  -- turn right
 374                  rotSpeedTarget      = self.winchRotateSpeed;
 375              end;
 376          end;
 377      end;
 378  
 379      -- update winch force
 380      if math.abs(self.ropeWinchForceTarget - self.ropeWinchForceLerp) > 1e-3 then
 381          local scale                 = clamp(0.2, 1, 1-self.ropeWinchForceLerp);
 382          self.ropeWinchForceLerp     = Utils.moveTowards(self.ropeWinchForceLerp, self.ropeWinchForceTarget, scale * self.ropeWinchForceSpeed*dt);
 383  
 384          local currentForce          = lerp(self.ropeWinchForceMin, self.ropeWinchForceMax, self.ropeWinchForceLerp);
 385          RopeWinch.setTensioningForce(self.ropeWinchNode, currentForce);
 386          RopeWinch.setSegmentLength(self.ropeWinchNode, lerp(0.5, 2, self.ropeWinchForceLerp));
 387      end;
 388  
 389      if self.anchorPointName ~= nil then
 390          rotSpeedTarget              = 0;
 391          self.winchCurrentRotSpeed   = 0;
 392      end;
 393  
 394      self.winchCurrentRotSpeed       = Utils.moveTowards(self.winchCurrentRotSpeed, rotSpeedTarget, self.winchRotateAcc*3*dt);
 395  
 396      if math.abs(self.winchCurrentRotSpeed) > 1e-3 then
 397          local rx, ry, rz            = getRotation(self.winchRoot);
 398          self:setWinchRootRotation(ry + self.winchCurrentRotSpeed * dt);
 399      end;
 400  end;

RopeWinch:getMayRopeWinchDisconnect()

Returns whether the winch may be disconnected right now (bool).

 404  function RopeWinch:getMayRopeWinchDisconnect()
 405      if self.anchorPointName ~= nil and self.anchorPointName ~= self.localPlayerIdentifier then
 406          -- we're attached to a real attacher
 407          return self.ropeWinchForceLerp < 0.1;
 408      end;
 409      return true;
 410  end;

RopeWinch:getRopeWinchConnectorActive()

Returns whether the control element located at the winch intake shall be active or not (bool).

 414  function RopeWinch:getRopeWinchConnectorActive()
 415      if self.anchorPointName ~= nil and self.anchorPointName ~= self.localPlayerIdentifier then
 416          -- don't click there if we're attached
 417          return false;
 418      end;
 419      if self.anchorPointName == self.localPlayerIdentifier then
 420          -- player may always decouple!
 421          return true;
 422      end;
 423      
 424      -- also don't click there if there's force on the rope
 425      if self.ropeWinchForceLerp > 0.1 then
 426          return false;
 427      end;
 428      -- show only if player controller is active
 429      return --[[ (g_scenario.player ~= nil and g_scenario.player.isActive) and 
 430  (AnchorPoint.activeVehicle == nil or (AnchorPoint.activeVehicle == self and self.anchorPointName == nil));
 431  end;

RopeWinch:ropeWinchConnectorCallback()

This is called every time the player clicks onto the control element at the winch intake. It may indicate that the player either wants to take the rope (and attach it to an anchor point) or to take in the rope.

 435  function RopeWinch:ropeWinchConnectorCallback()
 436      -- rope connector was hit
 437      if AnchorPoint.activeVehicle == self then
 438          -- turn off winch, no longer available for attaching
 439          self:attachWinchToAnchorPoint(nil);
 440          AnchorPoint.activeVehicle   = nil;
 441      else
 442          -- player wants to attach us
 443          AnchorPoint.activeVehicle   = self;
 444          self:attachWinchToAnchorPoint(self.localPlayerIdentifier);
 445  
 446          if g_scenario.tutorialAgent ~= nil then
 447              g_scenario.tutorialAgent:callByScript("attachRopeWinch");
 448          end;
 449      end;
 450  end;

RopeWinch:onEnter(player, isLocalPlayer)

Shows an info message when the player enters a winch vehicle for the first time.

 454  function RopeWinch:onEnter(player, isLocalPlayer)
 455      -- shoot out a help message
 456      if isLocalPlayer and g_scenario.tutorialAgent ~= nil then
 457          g_scenario.tutorialAgent:callByScript("winchSnowcat");
 458      end;
 459  end;

RopeWinch:setWinchRootRotation(rotation, noEvent)

Sets the winchRoot to the rotationY value.

 463  function RopeWinch:setWinchRootRotation(rotation, noEvent)
 464      setRotationY(self.winchRoot, rotation);
 465  
 466      if not noEvent then
 467          EventSetRopeWinchRotation:send(self, rotation);
 468      end;
 469  end;

RopeWinch:writeResync()

Resynchronizes all variables when a new player joins the game. The data sent by writeResync will be received by readResync.

 473  function RopeWinch:writeResync()
 474      if self.winchRoot == nil then
 475          return;
 476      end;
 477  
 478      local rx ,ry, rz        = getRotation(self.winchRoot);
 479      streamWriteFloat(ry);
 480      
 481      streamWriteBool(self.isTensed);
 482  
 483      streamWriteFloat(self.ropeWinchForceLerp or 0);
 484      
 485  end;

RopeWinch:readResync()

Resynchronizes all variables when a new player joins the game. The data sent by writeResync will be received by readResync.

 488  function RopeWinch:readResync()
 489      if self.winchRoot == nil then
 490          return;
 491      end;
 492      
 493      local rotY              = streamReadFloat();
 494      self:setWinchRootRotation(rotY, true);
 495  
 496      local tensed            = streamReadBool();
 497      self.ropeWinchForceLerp = streamReadFloat();
 498      self:setIsRopeTensed(tensed, true);
 499  end;

RopeWinch:postWriteResync()

postWriteResync is called after all writeResync calls of all entities have been processed. Do not use any streamWrite… functions in here, instead please send full events. These events will be executed right after readResync on the client, i.e. all entities will already be set up properly on the client before the events are executed.

We use this function to attach the rope hook for a newly joined player.

 504  function RopeWinch:postWriteResync()
 505      EventSetRopeWinchConnection:send(self, self.currentSetAnchorPoint);
 506  end;

All contents of this page may be used for modding use for Winter Resort Simulator - Season 2 only. Any use exceeding this regulation is not permitted.

Copyright (C) HR Innoways, 2021. All Rights Reserved.