• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Martomate / Hexacraft / 7351367576

28 Dec 2023 06:50PM UTC coverage: 51.185% (-0.1%) from 51.312%
7351367576

push

github

Martomate
Refactor: Made Chunk not know if it has been saved to file

9 of 12 new or added lines in 2 files covered. (75.0%)

110 existing lines in 32 files now uncovered.

2829 of 5527 relevant lines covered (51.19%)

0.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.38
/game/src/main/scala/hexacraft/game/Menus.scala
1
package hexacraft.game
2

3
import hexacraft.gui.{LocationInfo, RenderContext, Scene}
4
import hexacraft.gui.comp.{Button, Component, GUITransformation, Label, ScrollPane, SubComponents, TextField}
5
import hexacraft.infra.fs.{FileSystem, NbtIO}
6
import hexacraft.renderer.TextureSingle
7
import hexacraft.world.WorldSettings
8

9
import java.io.File
10
import java.nio.file.Path
11
import scala.util.Random
12

13
object Menus {
14

15
  abstract class MenuScene extends Scene with SubComponents {
×
16
    override def render(transformation: GUITransformation)(using context: RenderContext): Unit = {
×
17
      Component.drawImage(
18
        LocationInfo(-context.windowAspectRatio, -1, context.windowAspectRatio * 2, 2),
19
        transformation.x,
20
        transformation.y,
×
21
        TextureSingle.getTexture("textures/gui/menu/background"),
22
        context.windowAspectRatio
23
      )
×
24
      super.render(transformation)
25
    }
26
  }
27

28
  object WorldInfo {
×
29
    def fromFile(saveFile: File, fs: FileSystem): WorldInfo = {
×
30
      val nbtFile = saveFile.toPath.resolve("world.dat")
×
31
      val io = new NbtIO(fs)
×
32
      val (_, nbt) = io.loadTag(nbtFile.toFile)
33

34
      val name = nbt
×
35
        .getMap("general")
×
36
        .flatMap(general => general.getString("name"))
×
37
        .getOrElse(saveFile.getName)
38

×
39
      new WorldInfo(saveFile, name)
40
    }
41
  }
42

43
  case class WorldInfo(saveFile: File, name: String)
44

45
  object MainMenu {
46
    enum Event:
47
      case Play
48
      case Multiplayer
49
      case Settings
50
      case Quit
51
  }
52

53
  class MainMenu(multiplayerEnabled: Boolean)(onEvent: MainMenu.Event => Unit) extends MenuScene {
54
    import MainMenu.Event
55

1✔
56
    addComponent(new Label("Hexacraft", LocationInfo.from16x9(0, 0.8f, 1, 0.2f), 10).withColor(1, 1, 1))
1✔
57
    addComponent(Button("Play", LocationInfo.from16x9(0.4f, 0.55f, 0.2f, 0.1f))(onEvent(Event.Play)))
58

59
    if multiplayerEnabled then
1✔
60
      addComponent(Button("Multiplayer", LocationInfo.from16x9(0.4f, 0.4f, 0.2f, 0.1f))(onEvent(Event.Multiplayer)))
61

1✔
62
    addComponent(
1✔
63
      Button("Settings", LocationInfo.from16x9(0.4f, if multiplayerEnabled then 0.25f else 0.4f, 0.2f, 0.1f))(
1✔
64
        onEvent(Event.Settings)
65
      )
66
    )
1✔
67
    addComponent(Button("Quit", LocationInfo.from16x9(0.4f, 0.05f, 0.2f, 0.1f))(onEvent(Event.Quit)))
68
  }
69

70
  object HostWorldChooserMenu {
71
    enum Event:
72
      case Host(worldInfo: WorldInfo)
73
      case GoBack
74
  }
75

76
  class HostWorldChooserMenu(saveFolder: File, fs: FileSystem)(onEvent: HostWorldChooserMenu.Event => Unit)
77
      extends MenuScene {
78

79
    import HostWorldChooserMenu.Event
80

1✔
81
    addComponent(new Label("Choose world", LocationInfo.from16x9(0, 0.85f, 1, 0.15f), 6).withColor(1, 1, 1))
82

1✔
83
    private val scrollPane = new ScrollPane(LocationInfo.from16x9(0.285f, 0.225f, 0.43f, 0.635f), 0.025f * 2)
84

×
85
    for (f, i) <- getWorlds.zipWithIndex
86
    do
×
87
      scrollPane.addComponent(
×
88
        Button(f.name, LocationInfo.from16x9(0.3f, 0.75f - 0.1f * i, 0.4f, 0.075f)) {
×
89
          onEvent(Event.Host(f))
90
          // TODO: the network manager should repeatedly connect to the server registry.
91
          //  This will be blocking until a client wants to connect or after a timeout
92
          //  If this is not done in a certain time period the server will be deregistered from the server registry
93
        }
94
      )
1✔
95
    addComponent(scrollPane)
96

1✔
97
    addComponent(Button("Back to menu", LocationInfo.from16x9(0.3f, 0.05f, 0.4f, 0.1f))(onEvent(Event.GoBack)))
98

1✔
99
    private def getWorlds: Seq[WorldInfo] =
1✔
100
      val baseFolder = new File(saveFolder, "saves")
1✔
101
      if baseFolder.exists() then
×
102
        baseFolder
×
103
          .listFiles()
×
104
          .filter(f => new File(f, "world.dat").exists())
×
105
          .map(saveFile => WorldInfo.fromFile(saveFile, fs))
×
106
          .toSeq
1✔
107
      else Seq.empty[WorldInfo]
108

109
  }
110

111
  object JoinWorldChooserMenu {
112
    enum Event:
113
      case Join(address: String, port: Int)
114
      case GoBack
115

116
    private case class OnlineWorldInfo(id: Long, name: String, description: String)
117

118
    private case class OnlineWorldConnectionDetails(address: String, port: Int, time: Long)
119
  }
120

121
  class JoinWorldChooserMenu(onEvent: JoinWorldChooserMenu.Event => Unit) extends MenuScene {
122

123
    import JoinWorldChooserMenu.*
124

1✔
125
    addComponent(new Label("Choose world", LocationInfo.from16x9(0, 0.85f, 1, 0.15f), 6).withColor(1, 1, 1))
1✔
126
    private val scrollPane = new ScrollPane(LocationInfo.from16x9(0.285f, 0.225f, 0.43f, 0.635f), 0.025f * 2)
1✔
127
    addComponent(scrollPane)
128

1✔
129
    addComponent(Button("Back to menu", LocationInfo.from16x9(0.3f, 0.05f, 0.4f, 0.1f))(onEvent(Event.GoBack)))
130

1✔
131
    updateServerList()
132

1✔
133
    private def updateServerList(): Unit =
×
134
      for (f, i) <- getWorlds.zipWithIndex
135
      do
1✔
136
        scrollPane.addComponent(
1✔
137
          Button(f.name, LocationInfo.from16x9(0.3f, 0.75f - 0.1f * i, 0.4f, 0.075f)) {
×
138
            val connectionDetails = loadOnlineWorld(f.id)
×
139
            onEvent(Event.Join(connectionDetails.address, connectionDetails.port))
140
          }
141
        )
142

1✔
UNCOV
143
    private def getWorlds: Seq[OnlineWorldInfo] =
×
144
      Seq(
1✔
145
        OnlineWorldInfo(new Random().nextLong(), "Test Online World", "Welcome to my test world!"),
1✔
146
        OnlineWorldInfo(new Random().nextLong(), "Another Online World", "Free bitcakes!")
147
      )
148

×
149
    private def loadOnlineWorld(id: Long): OnlineWorldConnectionDetails =
150
      // TODO: connect to the server registry to get this information
151
      OnlineWorldConnectionDetails(
152
        "localhost",
153
        1234,
×
154
        System.currentTimeMillis() + 10
155
      )
156
  }
157

158
  object MultiplayerMenu {
159
    enum Event:
160
      case Join
161
      case Host
162
      case GoBack
163
  }
164

165
  class MultiplayerMenu(onEvent: MultiplayerMenu.Event => Unit) extends MenuScene {
166
    import MultiplayerMenu.Event
167

1✔
168
    addComponent(new Label("Multiplayer", LocationInfo.from16x9(0, 0.8f, 1, 0.2f), 10).withColor(1, 1, 1))
1✔
169
    addComponent(Button("Join", LocationInfo.from16x9(0.4f, 0.55f, 0.2f, 0.1f))(onEvent(Event.Join)))
1✔
170
    addComponent(Button("Host", LocationInfo.from16x9(0.4f, 0.4f, 0.2f, 0.1f))(onEvent(Event.Host)))
1✔
171
    addComponent(Button("Back", LocationInfo.from16x9(0.4f, 0.05f, 0.2f, 0.1f))(onEvent(Event.GoBack)))
172
  }
173

174
  class SettingsMenu(onBack: () => Unit) extends MenuScene {
1✔
175
    addComponent(Button("Coming soon!", LocationInfo.from16x9(0.4f, 0.55f, 0.2f, 0.1f)) {
×
176
      println("Settings will be implemented soon")
177
    })
1✔
178
    addComponent(Button("Back to menu", LocationInfo.from16x9(0.4f, 0.25f, 0.2f, 0.1f)) {
1✔
179
      onBack()
180
    })
181
  }
182

183
  object WorldChooserMenu {
184
    enum Event:
185
      case StartGame(saveDir: File, settings: WorldSettings)
186
      case CreateNewWorld
187
      case GoBack
188
  }
189

190
  class WorldChooserMenu(saveFolder: File, fs: FileSystem)(onEvent: WorldChooserMenu.Event => Unit) extends MenuScene {
191
    import WorldChooserMenu.Event
192

1✔
193
    addComponent(
1✔
194
      new Label("Choose world", LocationInfo.from16x9(0, 0.85f, 1, 0.15f), 6).withColor(1, 1, 1)
195
    )
196

1✔
197
    addComponent(makeScrollPane)
198

1✔
199
    addComponent(Button("Back to menu", LocationInfo.from16x9(0.3f, 0.05f, 0.19f, 0.1f)) {
1✔
200
      onEvent(Event.GoBack)
201
    })
1✔
202
    addComponent(Button("New world", LocationInfo.from16x9(0.51f, 0.05f, 0.19f, 0.1f)) {
1✔
203
      onEvent(Event.CreateNewWorld)
204
    })
205

1✔
206
    private def makeScrollPane: ScrollPane = {
1✔
207
      val scrollPaneLocation = LocationInfo.from16x9(0.3f, 0.25f, 0.4f, 0.575f).expand(0.025f * 2)
1✔
208
      val scrollPane = new ScrollPane(scrollPaneLocation, 0.025f * 2)
209

×
UNCOV
210
      val buttons = for ((f, i) <- getWorlds.zipWithIndex) yield makeWorldButton(f, i)
×
211
      for (b <- buttons) scrollPane.addComponent(b)
212

213
      scrollPane
214
    }
215

×
216
    private def makeWorldButton(world: WorldInfo, listIndex: Int): Button = {
×
217
      val buttonLocation = LocationInfo.from16x9(0.3f, 0.75f - 0.1f * listIndex, 0.4f, 0.075f)
218

×
219
      Button(world.name, buttonLocation) {
×
220
        onEvent(Event.StartGame(world.saveFile, WorldSettings.none))
221
      }
222
    }
223

1✔
224
    private def getWorlds: Seq[WorldInfo] = {
1✔
225
      val baseFolder = new File(saveFolder, "saves")
1✔
226
      if (fs.exists(baseFolder.toPath)) {
×
227
        for (saveFile <- saveFoldersSortedBy(baseFolder, p => -fs.lastModified(p).toEpochMilli))
×
228
          yield WorldInfo.fromFile(saveFile.toFile, fs)
1✔
229
      } else {
1✔
230
        Seq.empty[WorldInfo]
231
      }
232
    }
233

×
234
    private def saveFoldersSortedBy[S](baseFolder: File, sortFunc: Path => S)(using
235
        Ordering[S]
236
    ): Seq[Path] = {
×
237
      fs.listFiles(baseFolder.toPath)
×
238
        .map(worldFolder => (worldFolder, worldFolder.resolve("world.dat")))
×
239
        .filter(t => fs.exists(t._2))
×
240
        .sortBy(t => sortFunc(t._2))
×
241
        .map(_._1)
242
    }
243
  }
244

245
  object NewWorldMenu {
246
    enum Event:
247
      case StartGame(saveDir: File, settings: WorldSettings)
248
      case GoBack
249
  }
250

251
  class NewWorldMenu(saveFolder: File)(onEvent: NewWorldMenu.Event => Unit) extends MenuScene {
252
    import NewWorldMenu.Event
253

1✔
254
    addComponent(
1✔
255
      new Label("World name", LocationInfo.from16x9(0.3f, 0.7f + 0.075f, 0.2f, 0.05f), 3f, false)
1✔
256
        .withColor(1, 1, 1)
257
    )
1✔
258
    private val nameTF = new TextField(LocationInfo.from16x9(0.3f, 0.7f, 0.4f, 0.075f), maxFontSize = 2.5f)
1✔
259
    addComponent(nameTF)
260

1✔
261
    addComponent(
1✔
262
      new Label("World size", LocationInfo.from16x9(0.3f, 0.55f + 0.075f, 0.2f, 0.05f), 3f, false)
1✔
263
        .withColor(1, 1, 1)
264
    )
265
    private val sizeTF =
1✔
266
      new TextField(LocationInfo.from16x9(0.3f, 0.55f, 0.4f, 0.075f), maxFontSize = 2.5f)
1✔
267
    addComponent(sizeTF)
268

1✔
269
    addComponent(
1✔
270
      new Label("World seed", LocationInfo.from16x9(0.3f, 0.4f + 0.075f, 0.2f, 0.05f), 3f, false)
1✔
271
        .withColor(1, 1, 1)
272
    )
1✔
273
    private val seedTF = new TextField(LocationInfo.from16x9(0.3f, 0.4f, 0.4f, 0.075f), maxFontSize = 2.5f)
1✔
274
    addComponent(seedTF)
275

1✔
276
    addComponent(Button("Cancel", LocationInfo.from16x9(0.3f, 0.05f, 0.19f, 0.1f)) {
1✔
277
      onEvent(Event.GoBack)
278
    })
1✔
279
    addComponent(Button("Create world", LocationInfo.from16x9(0.51f, 0.05f, 0.19f, 0.1f))(createWorld()))
280

×
281
    private def createWorld(): Unit = {
×
282
      try {
×
283
        val baseFolder = new File(saveFolder, "saves")
×
284
        val file = uniqueFile(baseFolder, cleanupFileName(nameTF.text))
×
285
        val size = sizeTF.text.toByteOption.filter(s => s >= 0 && s <= 20)
×
286
        val seed = Some(seedTF.text)
×
287
          .filter(_.nonEmpty)
×
288
          .map(s => s.toLongOption.getOrElse(new Random(s.##.toLong << 32 | s.reverse.##).nextLong()))
289

×
290
        onEvent(Event.StartGame(file, WorldSettings(Some(nameTF.text), size, seed)))
291
      } catch {
×
292
        case _: Exception =>
293
        // TODO: complain about the input
294
      }
295
    }
296

×
297
    private def uniqueFile(baseFolder: File, fileName: String): File = {
298
      var file: File = null
299
      var count = 0
300
      while
301
        count += 1
×
302
        val name = if (count == 1) fileName else fileName + " " + count
×
303
        file = new File(baseFolder, name)
×
304
        file.exists()
305
      do ()
306

307
      file
308
    }
309

×
310
    private def cleanupFileName(fileName: String): String = {
×
311
      def charValid(c: Char): Boolean =
312
        c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == ' '
313

×
314
      val name = fileName.map(c => if (charValid(c)) c else '_').trim
×
315
      if (name.nonEmpty) name else "New World"
316
    }
317
  }
318

319
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc