About Monkey 2 › Forums › Monkey 2 Code Library › 3D Grid-based movement
Tagged: mojo3d
This topic contains 7 replies, has 3 voices, and was last updated by
Mark Sibly
1 year, 2 months ago.
-
AuthorPosts
-
August 2, 2017 at 1:27 am #9658
A “simple” demo of grid-based movement through a 3D maze. (Trying to simulate the old Dungeon Master / Eye of the Beholder style of movement).
EDIT (28 Jan 2018): Numerous fixes so code compiles with latest mojo3d.
Monkey123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869#rem3d grid-based movement within a procedurally generated maze.#end#Import "<std>"#Import "<mojo>"#Import "<mojo3d>"Using libc..Using std..Using mojo..Using mojo3d..Function Main()New AppInstanceNew MyWindow("Retro Dungeon Test", 1024, 768, WindowFlags.Fullscreen)App.Run()End' --- MOJO WINDOW ----------------------Class MyWindow Extends WindowField _scene:SceneField _light:LightField _retroCam:RetroCamField _keyBinder:KeyBinderField _world3d:World3dMethod New( title:String="Simple mojo app",width:Int=1024,height:Int=768,flags:WindowFlags = WindowFlags.Resizable | WindowFlags.HighDPI)Super.New( title,width,height,flags )_scene = Scene.GetCurrent()_scene.ClearColor = Color.Black_scene.AmbientLight = Color.DarkGrey'The black fog provides atmosphere and hides the 'pop-in' effect of visible/hidden cells._scene.FogColor = Color.Black_scene.FogNear = Cell3d.CellSize.X_scene.FogFar = Cell3d.CellSize.X * 3_keyBinder = New KeyBinder()_retroCam = New RetroCam(_scene)'The light is not yet properly functional._light=New Light(_retroCam.Camera)_light.Color = Color.Orange_light.Type = LightType.Directional_light.Range = Cell3d.CellSize.X * 4.0SeedRnd(Microsecs())Generate()EndMethod Generate:Void()Print "Generating maze. Please wait."Local ms:=Millisecs()'32 columns and 32 rows._world3d = New World3d(32, 32, _scene)Print "Generation time (millisecs): " + String(Millisecs() - ms)'3d cells, starting column and row, and a facing direction._retroCam.SetCells3d(_world3d.Cells, 1, 1, Dir.North)EndMethod OnUpdate:Void()HandleKeys()HandleMouse()_retroCam.Update()EndMethod HandleMouse:Void()If Mouse.Wheel.Y <> 0'Look up and down._retroCam.DoPitch(Float(Mouse.Wheel.Y) * 10)EndifIf Mouse.ButtonDown(MouseButton.Middle)'Reset the pitch._retroCam.ResetPitch()Elseif Mouse.ButtonDown(MouseButton.Left)If Not _retroCam.Moving And Not _retroCam.Turning'Left click on screen/window edges will move or turn depending on mouse location.If Mouse.Location.X < (0.1 * Width)If Mouse.Location.Y > Height - (0.2 * Height)_retroCam.StrafeLeft()Else_retroCam.DoCameraTurn(True)EndIfElseif Mouse.Location.X > Width - (0.1 * Width)If Mouse.Location.Y > Height - (0.2 * Height)_retroCam.StrafeRight()Else_retroCam.DoCameraTurn(False)EndIfElseif Mouse.Location.Y < (0.1 * Height)_retroCam.MoveForward()Elseif Mouse.Location.Y > Height - (0.1 * Height)_retroCam.MoveBack()EndifEndifEndifEndMethod HandleKeys:Void()If Not _keyBinder Then Return'Only process input if camera not moving or turning.If (Not _retroCam.Moving) And (Not _retroCam.Turning)If Keyboard.KeyDown(_keyBinder.Get(Command.ExitGame))App.Terminate()EndifIf Keyboard.KeyDown(_keyBinder.Get(Command.TurnLeft))_retroCam.DoCameraTurn(True)ReturnElseif Keyboard.KeyDown(_keyBinder.Get(Command.TurnRight))_retroCam.DoCameraTurn(False)ReturnEndifIf Keyboard.KeyDown(_keyBinder.Get(Command.MoveForward))_retroCam.MoveForward()ReturnEndifIf Keyboard.KeyDown(_keyBinder.Get(Command.MoveBack))_retroCam.MoveBack()ReturnEndifIf Keyboard.KeyDown(_keyBinder.Get(Command.StrafeRight))_retroCam.StrafeRight()ReturnEndifIf Keyboard.KeyDown(_keyBinder.Get(Command.StrafeLeft))_retroCam.StrafeLeft()ReturnEndifIf Keyboard.KeyDown(_keyBinder.Get(Command.Generate))Generate()EndifEndifEndMethod OnRender:Void(canvas:Canvas) OverrideRequestRender()'******OnUpdate()'******_scene.Update()_retroCam.Camera.Render(canvas)'_scene.Render(canvas, _retroCam.Camera)canvas.DrawText("FPS: "+ App.FPS, 10, 10)canvas.DrawText("Movement: WASD; Turn: QE; Pitch: Mouse wheel.", 10, 30)EndEnd' --- UTILS ----------------------Enum DirNone = 0NorthEastSouthWestLengthEnd' --- KEY BINDER ------------------Enum CommandExitGame = 0MoveForwardMoveBackStrafeLeftStrafeRightTurnLeftTurnRightGenerateEndClass KeyBinder Extends Map<Command, Key>PublicMethod New()Super.New()InitWASD()EndMethod InitWASD:Void()Add(Command.ExitGame, Key.Escape)Add(Command.MoveForward, Key.W | Key.Raw)Add(Command.MoveBack, Key.S | Key.Raw)Add(Command.StrafeLeft, Key.A | Key.Raw)Add(Command.StrafeRight, Key.D | Key.Raw)Add(Command.TurnLeft, Key.Q | Key.Raw)Add(Command.TurnRight, Key.E | Key.Raw)Add(Command.Generate, Key.Space | Key.Raw)EndEnd' --- WORLD 3D ------------------Class World3d'In this example all 3D objects (except camera and lights) are stored in this class.'Objects are organized into a grid to assist with collision testing and visibility.Field _cells:Cell3d[,]Field _cols:Int, _rows:IntMethod New(cols:Int, rows:Int, scene:Scene)_cols = cols_rows = rowsIf _cols < 8 Then _cols = 8If _rows < 8 Then _rows = 8Local arr2d := New Barrier[_cols, _rows]GenerateMaze(arr2d)_cells = New Cell3d[_cols, _rows]For Local c := 0 Until _colsFor Local r := 0 Until _rows_cells[c, r] = New Cell3d(c, r, arr2d[c, r])'Need to explicitly hide models because they are shown by default when created._cells[c, r].Hide()NextNextEndProperty Cols:Int()Return _colsEndProperty Rows:Int()Return _rowsEndProperty Cells:Cell3d[,]()Return _cellsEndEnd'----Class Cell3dPublic'Could probably set this to anything "realistic".Global CellSize:Vec3f = New Vec3f(6.0, 6.0, 6.0)Method New(col:Int, row:Int, barrier:Barrier)_col = col_row = row_barrier = barrier_centres = New Vec3f[Dir.Length]CalcCentres()CreateModels()EndProperty Blocked:Bool()Return _blockedSetter (value:Bool)_blocked = valueEndMethod CalcCentres:Void()'These "centres" are used by the camera, depending on its facing direction.Local cx := (Float(_row) * CellSize.X) + (CellSize.X * 0.5)Local cy := (Cell3d.CellSize.Y * 0.66)Local cz := (Float(_col) * CellSize.Z) + (CellSize.Z * 0.5)SetCentre(Dir.None, cx, cy, cz)SetCentre(Dir.West, cx + (CellSize.X * 0.25), cy, cz)SetCentre(Dir.North, cx, cy, cz - (CellSize.Z * 0.25))SetCentre(Dir.East, cx - (CellSize.X * 0.25), cy, cz)SetCentre(Dir.South, cx, cy, cz + (CellSize.Z * 0.25))EndMethod SetCentre:Void(dir:Dir, x:Float, y:Float, z:Float)If (dir < 0) Or (dir >= _centres.Length) Then Return_centres[dir] = New Vec3f(x, y, z)EndMethod GetCentre:Vec3f(dir:Dir)If (dir < Dir.None) Or (dir >= _centres.Length) Then Return New Vec3f(-1, -1, -1)Return _centres[dir]EndMethod CreateModels:Void()'Using simple colored boxes for the floors, ceilings and walls.Select _barrierCase Barrier.None_blocked = False_block = Null_floor = New Model(Mesh.CreateBox(New Boxf(0.0, 0.0, 0.0, CellSize.X, 0.2, CellSize.Z)), New PbrMaterial(New Color(1.0, Rnd(), Rnd()), 0.5, 0.5))_floor.Position = New Vec3f(_row * CellSize.X, -0.20, _col * CellSize.Z)_ceil = New Model(Mesh.CreateBox(New Boxf(0.0, 0.0, 0.0, CellSize.X, 0.2, CellSize.Z)), New PbrMaterial(New Color(Rnd(), 1.0, Rnd()), 0.5, 0.5))_ceil.Position = New Vec3f(_row * CellSize.X, CellSize.Y, _col * CellSize.Z)Default_floor = Null_ceil = Null_blocked = True_block = New Model(Mesh.CreateBox(New Boxf(0.0, 0.0, 0.0, CellSize.X, CellSize.Y, CellSize.Z)), New PbrMaterial(New Color(Rnd(), Rnd(), 1.0), 0.5, 0.5))_block.Position = New Vec3f(_row * CellSize.X, 0.0, _col * CellSize.Z)EndEndMethod Show:Void()'Show everything in the cell.If _block Then _block.Visible = TrueIf _floor Then _floor.Visible = TrueIf _ceil Then _ceil.Visible = TrueEndMethod Hide:Void()'Hide everything in the cell.If _block Then _block.Visible = FalseIf _floor Then _floor.Visible = FalseIf _ceil Then _ceil.Visible = FalseEndPrivateField _block:ModelField _floor:ModelField _ceil:ModelField _blocked:BoolField _centres:Vec3f[]Field _col:Int, _row:IntField _barrier:BarrierEnd' --- RETRO STYLE DUNGEON CAMERA ------------------'Grid movement ala Dungeon Master, Eye of the Beholder, et al.Class RetroCamPublicMethod New(scene:Scene)_scene = scene_camera = New Camera()_camera.Near = 0.01_camera.Far = 100.0_camPath = New Vec3f[16]_camPathIdx = -1_camMoving = False_camTurnLeft = New Float[_camPath.Length]_camTurnRight = New Float[_camPath.Length]For Local i:Int = 0 Until _camPath.Length_camTurnLeft[i] = 90.0 / _camPath.Length_camTurnRight[i] = -_camTurnLeft[i]Next_camTurnIdx = -1_camTurning = False_viewRange = 5_visibleCells = New Stack<Cell3d>_pitchClamp = New Vec2f(-70.0, 70)FlipPitch = TrueEndProperty Camera:Camera()Return _cameraEndProperty FlipPitch:Bool()Return _flipPitchSetter (value:Bool)_flipPitch = valueEndProperty Moving:Bool()Return _camMovingEndProperty Turning:Bool()Return _camTurningEndMethod SetCells3d:Void(cells3d:Cell3d[,], col:Int, row:Int, facingDir:Dir)If Not cells3d Then Return_cells3d = cells3dSetCoord(col, row, facingDir)EndMethod SetCoord:Void(col:Int, row:Int, facingDir:Dir)_facingDir = facingDir_cameraCol = col_cameraRow = row_camera.Position = _cells3d[_cameraCol, _cameraRow].GetCentre(_facingDir)ResetYaw()DoCameraMove(0, 0)EndMethod Update:Void()'Perform camera turning operationIf _camTurning_camera.RotateY(_camTurn[_camTurnIdx])_camTurnIdx += 1If _camTurnIdx >= _camTurn.Length_camTurning = FalseEndifEndif'Perform camera moving operationIf _camMoving_camera.Position = _camPath[_camPathIdx]_camPathIdx += 1If _camPathIdx >= _camPath.Length_camMoving = False_camera.Position = _cells3d[_cameraCol, _cameraRow].GetCentre(_facingDir)CalcVisibleCells()EndifEndifEndMethod StrafeLeft:Void()Select _facingDirCase Dir.NorthDoCameraMove(0, -1)Case Dir.EastDoCameraMove(1, 0)Case Dir.SouthDoCameraMove(0, 1)Case Dir.WestDoCameraMove(-1, 0)EndEndMethod StrafeRight:Void()Select _facingDirCase Dir.NorthDoCameraMove(0, 1)Case Dir.EastDoCameraMove(-1, 0)Case Dir.SouthDoCameraMove(0, -1)Case Dir.WestDoCameraMove(1, 0)EndEndMethod MoveBack:Void()Select _facingDirCase Dir.NorthDoCameraMove(-1, 0)Case Dir.EastDoCameraMove(0, -1)Case Dir.SouthDoCameraMove(1, 0)Case Dir.WestDoCameraMove(0, 1)EndEndMethod MoveForward:Void()Select _facingDirCase Dir.NorthDoCameraMove(1, 0)Case Dir.EastDoCameraMove(0, 1)Case Dir.SouthDoCameraMove(-1, 0)Case Dir.WestDoCameraMove(0, -1)EndEndMethod ResetPitch:Void()Local camrot := _camera.Rotation_camera.Rotation = New Vec3f(0.0, camrot.Y, camrot.Z)EndMethod ResetYaw:Void()Local camrot := _camera.RotationLocal yaw:FloatSelect _facingDirCase Dir.Northyaw = 0.0Case Dir.Eastyaw = 90.0Case Dir.Southyaw = 180.0Case Dir.Westyaw = 270.0End_camera.Rotation = New Vec3f(camrot.X, yaw, camrot.Z)EndMethod DoPitch:Void(amount:Float)Local camrot := _camera.RotationLocal newRotX:Float = _flipPitch ? camrot.X + amount Else camrot.X - amountnewRotX = Clamp<Float>(newRotX, _pitchClamp.X, _pitchClamp.Y)_camera.Rotation = New Vec3f(newRotX, camrot.Y, camrot.Z)EndMethod DoCameraMove:Void(cOffset:Int, rOffset:Int)Local newCol := _cameraCol + cOffsetLocal newRow := _cameraRow + rOffsetIf newCol < 0 Or newRow < 0 Or newCol >= _cells3d.GetSize(0) Or newRow >= _cells3d.GetSize(1) Then ReturnIf Not(_cells3d[newCol, newRow].Blocked)'If the cell is not blocked then move into it.'A linear path is calculated for a 'scrolling' effect._cameraCol = newCol_cameraRow = newRowLocal camX := _camera.XLocal camZ := _camera.ZLocal targ := _cells3d[newCol, newRow].GetCentre(_facingDir)Local diffX := camX - targ.XLocal diffZ := camZ - targ.ZFor Local pathIdx := 0 Until _camPath.Length_camPath[pathIdx].x = camX - (pathIdx * (diffX / _camPath.Length))_camPath[pathIdx].z = camZ - (pathIdx * (diffZ / _camPath.Length))_camPath[pathIdx].y = (Cell3d.CellSize.Y * 0.66)Next_camPathIdx = 0_camMoving = TrueEndifEnd'Turn camera, either left or right.Method DoCameraTurn:Void(left:Bool)ResetPitch()If left_facingDir = Cast<Dir>(Cast<Int>(_facingDir) - 1)If _facingDir <= Dir.None Then _facingDir = Dir.West_camTurn = _camTurnLeftElse_facingDir = Cast<Dir>(Cast<Int>(_facingDir) + 1)If _facingDir >= Dir.Length Then _facingDir = Dir.North_camTurn = _camTurnRightEndif'need to 'tweak' the camera's x, z position based on the new facing direction.DoCameraMove(0, 0)_camTurnIdx = 0_camTurning = TrueEnd'Hide entities outside the camera's view range. This improves performance significantly.Method CalcVisibleCells:Void()If Not _cells3d Then ReturnLocal cols := _cells3d.GetSize(0)Local rows := _cells3d.GetSize(1)For Local cell := Eachin _visibleCellscell.Hide()Next_visibleCells.Clear()For Local c := _cameraCol - _viewRange To _cameraCol + _viewRangeFor Local r := _cameraRow - _viewRange To _cameraRow + _viewRangeIf c >= 0 And c < cols And r >= 0 And r < rows_visibleCells.Push(_cells3d[c, r])_cells3d[c, r].Show()EndifNextNextEndPrivateField _camera:CameraField _scene:SceneField _facingDir:Dir 'Camera's current facing direction. This is critical for'positioning, turning and movement calculations.Field _cameraCol:IntField _cameraRow:IntField _camPath:Vec3f[] 'Current camera movement path.Field _camPathIdx:Int 'Index position of current camera movement path.Field _camMoving:Bool 'Camera is moving?Field _camTurn:Float[] 'Current camera turning path.Field _camTurnLeft:Float[] 'Pre-calculated left camera turning path.Field _camTurnRight:Float[] 'Pre-calculated right camera turning path.Field _camTurnIdx:Int 'Index position of current camera turning path.Field _camTurning:Bool 'Camera is turning?Field _cells3d:Cell3d[,] 'Reference to a grid of 3d cells.Field _viewRange:Int 'number of cellsField _visibleCells:Stack<Cell3d> 'visible cells (calculated upon movement command).Field _pitchClamp:Vec2f 'Restrict the camera pitch minimum and maxumum.Field _flipPitch:BoolEnd' --- MAZE GENERATOR ------------------Enum BarrierNone = 0BlockEndStruct CellPublicField North:Bool = TrueField East:Bool = TrueField South:Bool = TrueField West:Bool = TrueEndFunction GenerateMaze:Void(arr2d:Barrier[,])'Generate a perfect maze (every coordinate is reachable by every other coordinate) within a 2D array.If Not arr2d Then ReturnLocal cols:Int = arr2d.GetSize(0)Local rows:Int = arr2d.GetSize(1)Local cellCols:IntIf (cols Mod 2 <> 0) Then cellCols = Floor(cols / 2) Else cellCols = Floor((cols - 1) / 2)Local cellRows:IntIf (rows Mod 2 <> 0) Then cellRows = Floor(rows / 2) Else cellRows = Floor((rows - 1) / 2)Local totalCells:Int = cellRows * cellColsLocal cellMaze:= New Cell[cellCols, cellRows]Local track := New Vec2i[totalCells]Local tri:Int = 0Local dir:= New Vec2i[4], nextCoord := New Vec2i[4]Local curCoord:Vec2iLocal visited:int, i:Int, mazei:Int, nbi:Int, r:Int, c:IntFor r = 0 Until cellRowsFor c = 0 Until cellColsi = (r * cellCols) + ccellMaze[c, r] = New Cell()track[i] = New Vec2iNextNextFor i = 0 Until dir.Lengthdir[i] = New Vec2inextCoord[i] = New Vec2iNextdir[0].Y = -1 'northdir[1].X = 1 'eastdir[2].Y = 1 'southdir[3].X = -1 'westcurCoord.Y = Floor(Rnd(0, cellRows))curCoord.X = Floor(Rnd(0, cellCols))visited = 1While (visited < totalCells)nbi = 0For i = 0 Until dir.Lengthr = curCoord.Y + dir[i].Yc = curCoord.X + dir[i].XIf (r >= 0) And (r < cellRows) And (c >= 0) And (c < cellCols)If (cellMaze[c, r].North) And (cellMaze[c, r].East) And (cellMaze[c, r].South) And (cellMaze[c, r].West)nextCoord[nbi].Y = rnextCoord[nbi].X = cnbi += 1EndifEndifNextif (nbi >= 1)i = Floor(Rnd(0, nbi))If (nextCoord[i].Y - curCoord.Y) = 0 Thenr = nextCoord[i].YIf nextCoord[i].X > curCoord.X Thenc = curCoord.XcellMaze[c, r].East = Falsec = nextCoord[i].XcellMaze[c, r].West = Falseelsec = curCoord.XcellMaze[c, r].West = Falsec = nextCoord[i].XcellMaze[c, r].East = Falseendifelsec = nextCoord[i].XIf nextCoord[i].Y > curCoord.Y Thenr = curCoord.YcellMaze[c, r].South = Falser = nextCoord[i].YcellMaze[c, r].North = FalseElser = curCoord.YcellMaze[c, r].North = Falser = nextCoord[i].YcellMaze[c, r].South = falseendifEndiftri += 1track[tri].Y = curCoord.Ytrack[tri].X = curCoord.XcurCoord.Y = nextCoord[i].YcurCoord.X = nextCoord[i].Xvisited += 1ElsecurCoord.Y = track[tri].YcurCoord.X = track[tri].Xtri -= 1EndifWend'--- Remap maze cells to 2d array.For Local r := 0 Until rowsFor Local c := 0 Until colsarr2d[c, r] = Barrier.BlockNextNextLocal mazer:Int, mazec:IntFor Local r := 0 Until cellRowsmazer = (r * 2) + 1For Local c := 0 Until cellColsmazec = (c * 2) + 1arr2d[mazec, mazer] = Barrier.NoneIf (Not cellMaze[c, r].West) Then arr2d[mazec - 1, mazer] = Barrier.NoneIf (Not cellMaze[c, r].North) Then arr2d[mazec, mazer - 1] = Barrier.NoneNextNextEndAugust 2, 2017 at 2:59 am #9662The Q and E buttons make the screen turn like crazy. Something wrong there on my system.
I also had a security message when I wanted to paste a error log using the code box. This was in the dev forum. I saw a fox image iirc.
August 2, 2017 at 3:11 am #9663The Q and E buttons make the screen turn like crazy. Something wrong there on my system.
You’r probably still using radians version of mojo3d – angular units will change to degrees in next release (tomorrow probably).
Nice demo though, although I’m a teeny bit disappointed it’s not a ‘real’ 3d maze!
I get “*** Forbidden: Message seems to be spam. ***
Yeah I need to work out what’s up with cleantalk, the antispam provider.
August 2, 2017 at 3:47 am #9664Yes, the demo is coded against the latest monkey 2 dev branch where mojo3d is now using degrees instead of radians…
@Mark
What do you mean by ‘real’ 3d maze? Multiple levels high, stacked like a tower? Or “free-form” movement?August 2, 2017 at 3:59 am #9665What do you mean by ‘real’ 3d maze?
A maze that goes from A to B where A and B are 3d points and corridors can go left/right/up/down!
August 2, 2017 at 5:26 am #9666Ahhh, like a “hypermaze”:
http://www.astrolog.org/labyrnth/maze/hyper.gif
http://www.astrolog.org/labyrnth/hypermaz.htmIndeed, that would be an absolute nightmare to navigate, though, even with a map.
January 28, 2018 at 8:59 am #13313There has been some mojo3d architectural changes since I first posted this example so I fixed it to run with the current mojo3d module.
Also, how can I remove the old file attachment from my initial post?
January 28, 2018 at 9:50 am #13315Also, how can I remove the old file attachment from my initial post?
Should be gone now?
Thanks for the update too!
-
AuthorPosts
You must be logged in to reply to this topic.