diff --git a/sprites/items/OcelotCard.png b/sprites/items/OcelotCard.png new file mode 100644 index 0000000..e2a6c21 Binary files /dev/null and b/sprites/items/OcelotCard.png differ diff --git a/src/main/resources/ocelot/desktop/colorscheme.txt b/src/main/resources/ocelot/desktop/colorscheme.txt index 4664a99..ffde8e5 100644 --- a/src/main/resources/ocelot/desktop/colorscheme.txt +++ b/src/main/resources/ocelot/desktop/colorscheme.txt @@ -154,4 +154,11 @@ Grid3DColor = #303030 Grid3DPosX = #c9205a Grid3DNegX = #6e213b Grid3DPosZ = #255cba -Grid3DNegZ = #264170 \ No newline at end of file +Grid3DNegZ = #264170 + +LogBackground = #222222 +LogBorder = #666666 +LogEntryBackground = #cccccc +LogEntryBorder = #888888 + +OcelotCardTooltip = #c1905f \ No newline at end of file diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png index f25d18b..af1740e 100644 Binary files a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png and b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png differ diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt index cc87645..6998abe 100644 --- a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt +++ b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt @@ -1,275 +1,276 @@ BackgroundPattern 0 0 304 304 -BarSegment 493 479 16 4 -Empty 473 277 16 16 -EmptySlot 233 379 18 18 -Knob 447 208 50 50 -KnobCenter 233 305 50 50 -KnobLimits 284 305 50 50 -ShadowBorder 497 305 1 24 -ShadowCorner 372 305 24 24 -TabArrow 85 496 8 14 -blocks/Generic 490 277 16 16 -blocks/HologramEffect 277 498 4 4 -blocks/HologramProjector1Top 486 129 16 16 -blocks/HologramProjector2Top 486 146 16 16 -blocks/HologramProjectorSide 486 163 16 16 -buttons/BottomDrawerClose 252 379 18 18 -buttons/BottomDrawerOpen 271 379 18 18 -buttons/OpenFMRadioCloseOff 248 496 7 8 -buttons/OpenFMRadioCloseOn 256 496 7 8 -buttons/OpenFMRadioRedstoneOff 203 496 8 8 -buttons/OpenFMRadioRedstoneOn 212 496 8 8 -buttons/OpenFMRadioStartOff 397 305 24 24 -buttons/OpenFMRadioStartOn 422 305 24 24 -buttons/OpenFMRadioStopOff 447 305 24 24 -buttons/OpenFMRadioStopOn 472 305 24 24 -buttons/OpenFMRadioVolumeDownOff 130 496 10 10 -buttons/OpenFMRadioVolumeDownOn 141 496 10 10 -buttons/OpenFMRadioVolumeUpOff 152 496 10 10 -buttons/OpenFMRadioVolumeUpOn 163 496 10 10 -buttons/PowerOff 290 379 18 18 -buttons/PowerOn 309 379 18 18 -icons/Antenna 486 180 16 16 -icons/ArrowRight 0 411 16 16 -icons/AspectRatio 17 411 16 16 -icons/ButtonCheck 447 259 17 17 -icons/ButtonClipboard 465 259 17 17 -icons/ButtonRandomize 483 259 17 17 -icons/CPU 34 411 16 16 -icons/Card 51 411 16 16 -icons/Close 0 496 15 14 -icons/Code 68 411 16 16 -icons/ComponentBus 85 411 16 16 -icons/Copy 102 411 16 16 -icons/Cross 119 411 16 16 -icons/Delete 136 411 16 16 -icons/DragLMB 302 356 21 14 -icons/DragRMB 324 356 21 14 -icons/EEPROM 153 411 16 16 -icons/Edit 170 411 16 16 -icons/Eject 187 411 16 16 -icons/File 204 411 16 16 -icons/Floppy 221 411 16 16 -icons/Folder 238 411 16 16 -icons/FolderSlash 255 411 16 16 -icons/Grid 233 356 22 22 -icons/GridOff 256 356 22 22 -icons/HDD 272 411 16 16 -icons/Home 279 356 22 22 -icons/LMB 61 496 11 14 -icons/Label 289 411 16 16 -icons/LinesHorizontal 306 411 16 16 -icons/Link 323 411 16 16 -icons/LinkSlash 340 411 16 16 -icons/Memory 357 411 16 16 -icons/Microchip 374 411 16 16 -icons/NA 391 411 16 16 -icons/NotificationError 94 496 11 11 -icons/NotificationInfo 106 496 11 11 -icons/NotificationWarning 118 496 11 11 -icons/Pin 16 496 14 14 -icons/Plus 408 411 16 16 -icons/Power 425 411 16 16 -icons/RMB 73 496 11 14 -icons/Restart 442 411 16 16 -icons/Save 459 411 16 16 -icons/SaveAs 476 411 16 16 -icons/Server 493 411 16 16 -icons/SettingsSound 447 277 12 17 -icons/SettingsUI 460 277 12 17 -icons/Tier0 0 428 16 16 -icons/Tier1 17 428 16 16 -icons/Tier2 34 428 16 16 -icons/Tiers 51 428 16 16 -icons/Unpin 31 496 14 14 -icons/WaveLFSR 335 344 24 10 -icons/WaveNoise 360 344 24 10 -icons/WaveSawtooth 385 344 24 10 -icons/WaveSine 410 344 24 10 -icons/WaveSquare 435 344 24 10 -icons/WaveTriangle 460 344 24 10 -icons/Window 68 428 16 16 -icons/WireArrowLeft 264 496 4 8 -icons/WireArrowRight 269 496 4 8 -items/APU0 305 208 16 96 -items/APU1 322 208 16 96 -items/APU2 339 208 16 96 -items/CPU0 85 428 16 16 -items/CPU1 102 428 16 16 -items/CPU2 119 428 16 16 -items/CardBase 136 428 16 16 -items/CircuitBoard 153 428 16 16 -items/ComponentBus0 170 428 16 16 -items/ComponentBus1 187 428 16 16 -items/ComponentBus2 204 428 16 16 -items/ComponentBus3 221 428 16 16 -items/DataCard0 434 0 16 128 -items/DataCard1 451 0 16 128 -items/DataCard2 468 0 16 128 -items/DebugCard 238 428 16 16 -items/DiskDriveMountable 255 428 16 16 -items/EEPROM 272 428 16 16 -items/FloppyDisk_dyeBlack 289 428 16 16 -items/FloppyDisk_dyeBlue 306 428 16 16 -items/FloppyDisk_dyeBrown 323 428 16 16 -items/FloppyDisk_dyeCyan 340 428 16 16 -items/FloppyDisk_dyeGray 357 428 16 16 -items/FloppyDisk_dyeGreen 374 428 16 16 -items/FloppyDisk_dyeLightBlue 391 428 16 16 -items/FloppyDisk_dyeLightGray 408 428 16 16 -items/FloppyDisk_dyeLime 425 428 16 16 -items/FloppyDisk_dyeMagenta 442 428 16 16 -items/FloppyDisk_dyeOrange 459 428 16 16 -items/FloppyDisk_dyePink 476 428 16 16 -items/FloppyDisk_dyePurple 493 428 16 16 -items/FloppyDisk_dyeRed 0 445 16 16 -items/FloppyDisk_dyeWhite 17 445 16 16 -items/FloppyDisk_dyeYellow 34 445 16 16 -items/GraphicsCard0 51 445 16 16 -items/GraphicsCard1 68 445 16 16 -items/GraphicsCard2 85 445 16 16 -items/HardDiskDrive0 102 445 16 16 -items/HardDiskDrive1 119 445 16 16 -items/HardDiskDrive2 136 445 16 16 -items/InternetCard 338 305 16 32 -items/LinkedCard 356 208 16 96 -items/Memory0 153 445 16 16 -items/Memory1 170 445 16 16 -items/Memory2 187 445 16 16 -items/Memory3 204 445 16 16 -items/Memory4 221 445 16 16 -items/Memory5 238 445 16 16 -items/NetworkCard 255 445 16 16 -items/RedstoneCard0 272 445 16 16 -items/RedstoneCard1 289 445 16 16 -items/SelfDestructingCard 355 305 16 32 -items/Server0 306 445 16 16 -items/Server1 323 445 16 16 -items/Server2 340 445 16 16 -items/Server3 357 445 16 16 -items/SoundCard 485 0 16 128 -items/WirelessNetworkCard0 374 445 16 16 -items/WirelessNetworkCard1 391 445 16 16 -light-panel/BookmarkLeft 328 379 18 14 -light-panel/BookmarkRight 346 356 20 14 -light-panel/BorderB 282 498 4 4 -light-panel/BorderL 372 498 4 2 -light-panel/BorderR 287 498 4 4 -light-panel/BorderT 292 498 4 4 -light-panel/CornerBL 297 498 4 4 -light-panel/CornerBR 302 498 4 4 -light-panel/CornerTL 307 498 4 4 -light-panel/CornerTR 312 498 4 4 -light-panel/Fill 507 484 2 2 -light-panel/Vent 335 305 2 38 -nodes/Cable 221 496 8 8 -nodes/Camera 408 445 16 16 -nodes/Chest 46 496 14 14 -nodes/HologramProjector0 425 445 16 16 -nodes/HologramProjector1 442 445 16 16 -nodes/IronNoteBlock 459 445 16 16 -nodes/Lamp 476 445 16 16 -nodes/LampFrame 493 445 16 16 -nodes/LampGlow 305 0 128 128 -nodes/NewNode 0 462 16 16 -nodes/NoteBlock 17 462 16 16 -nodes/OpenFMRadio 34 462 16 16 -nodes/Relay 51 462 16 16 -nodes/computer/Activity 68 462 16 16 -nodes/computer/Default 85 462 16 16 -nodes/computer/Error 102 462 16 16 -nodes/computer/On 119 462 16 16 -nodes/disk-drive/Activity 136 462 16 16 -nodes/disk-drive/Default 153 462 16 16 -nodes/disk-drive/Floppy 170 462 16 16 -nodes/rack/Default 187 462 16 16 -nodes/rack/Empty 204 462 16 16 -nodes/rack/drive/0/Activity 221 462 16 16 -nodes/rack/drive/0/Default 238 462 16 16 -nodes/rack/drive/0/Floppy 255 462 16 16 -nodes/rack/drive/1/Activity 272 462 16 16 -nodes/rack/drive/1/Default 289 462 16 16 -nodes/rack/drive/1/Floppy 306 462 16 16 -nodes/rack/drive/2/Activity 323 462 16 16 -nodes/rack/drive/2/Default 340 462 16 16 -nodes/rack/drive/2/Floppy 357 462 16 16 -nodes/rack/drive/3/Activity 374 462 16 16 -nodes/rack/drive/3/Default 391 462 16 16 -nodes/rack/drive/3/Floppy 408 462 16 16 -nodes/rack/drive/Floppy 425 462 16 16 -nodes/rack/server/0/Activity 442 462 16 16 -nodes/rack/server/0/Default 459 462 16 16 -nodes/rack/server/0/Error 476 462 16 16 -nodes/rack/server/0/On 493 462 16 16 -nodes/rack/server/1/Activity 0 479 16 16 -nodes/rack/server/1/Default 17 479 16 16 -nodes/rack/server/1/Error 34 479 16 16 -nodes/rack/server/1/On 51 479 16 16 -nodes/rack/server/2/Activity 68 479 16 16 -nodes/rack/server/2/Default 85 479 16 16 -nodes/rack/server/2/Error 102 479 16 16 -nodes/rack/server/2/On 119 479 16 16 -nodes/rack/server/3/Activity 136 479 16 16 -nodes/rack/server/3/Default 153 479 16 16 -nodes/rack/server/3/Error 170 479 16 16 -nodes/rack/server/3/On 187 479 16 16 -nodes/screen/BottomLeft 204 479 16 16 -nodes/screen/BottomMiddle 221 479 16 16 -nodes/screen/BottomRight 238 479 16 16 -nodes/screen/ColumnBottom 255 479 16 16 -nodes/screen/ColumnMiddle 272 479 16 16 -nodes/screen/ColumnTop 289 479 16 16 -nodes/screen/Middle 306 479 16 16 -nodes/screen/MiddleLeft 323 479 16 16 -nodes/screen/MiddleRight 340 479 16 16 -nodes/screen/PowerOnOverlay 357 479 16 16 -nodes/screen/RowLeft 374 479 16 16 -nodes/screen/RowMiddle 391 479 16 16 -nodes/screen/RowRight 408 479 16 16 -nodes/screen/Standalone 425 479 16 16 -nodes/screen/TopLeft 442 479 16 16 -nodes/screen/TopMiddle 459 479 16 16 -nodes/screen/TopRight 476 479 16 16 -panel/BorderB 317 498 4 4 -panel/BorderL 377 498 4 2 -panel/BorderR 322 498 4 4 -panel/BorderT 327 498 4 4 -panel/CornerBL 332 498 4 4 -panel/CornerBR 337 498 4 4 -panel/CornerTL 342 498 4 4 -panel/CornerTR 347 498 4 4 -panel/Fill 493 489 2 2 -particles/Note 192 496 7 10 -screen/BorderB 274 496 2 8 -screen/BorderT 200 496 2 10 -screen/CornerBL 230 496 8 8 -screen/CornerBR 239 496 8 8 -screen/CornerTL 174 496 8 10 -screen/CornerTR 183 496 8 10 -window/BorderDark 510 479 1 4 -window/BorderLight 493 484 1 4 -window/CornerBL 352 498 4 4 -window/CornerBR 357 498 4 4 -window/CornerTL 362 498 4 4 -window/CornerTR 367 498 4 4 -window/OpenFMRadio 0 305 232 105 -window/case/Motherboard 406 129 79 70 -window/rack/Lines 373 208 73 91 -window/rack/Motherboard 305 129 100 78 -window/rack/NetworkBack 496 489 1 2 -window/rack/NetworkBottom 498 489 1 2 -window/rack/NetworkConnector 500 489 1 2 -window/rack/NetworkLeft 502 489 1 2 -window/rack/NetworkRight 504 489 1 2 -window/rack/NetworkTop 506 489 1 2 -window/rack/NodeBack 277 496 5 1 -window/rack/NodeBottom 283 496 5 1 -window/rack/NodeLeft 289 496 5 1 -window/rack/NodeRight 295 496 5 1 -window/rack/NodeTop 301 496 5 1 -window/rack/SideBack 495 484 1 3 -window/rack/SideBottom 497 484 1 3 -window/rack/SideConnector 499 484 1 3 -window/rack/SideLeft 501 484 1 3 -window/rack/SideRight 503 484 1 3 -window/rack/SideTop 505 484 1 3 +BarSegment 29 499 16 4 +Empty 153 381 16 16 +EmptySlot 256 330 18 18 +Knob 692 0 50 50 +KnobCenter 743 0 50 50 +KnobLimits 794 0 50 50 +ShadowBorder 0 538 1 24 +ShadowCorner 153 305 24 24 +TabArrow 0 484 8 14 +blocks/Generic 170 381 16 16 +blocks/HologramEffect 8 547 4 4 +blocks/HologramProjector1Top 187 381 16 16 +blocks/HologramProjector2Top 204 381 16 16 +blocks/HologramProjectorSide 221 381 16 16 +buttons/BottomDrawerClose 275 330 18 18 +buttons/BottomDrawerOpen 294 330 18 18 +buttons/OpenFMRadioCloseOff 48 510 7 8 +buttons/OpenFMRadioCloseOn 56 510 7 8 +buttons/OpenFMRadioRedstoneOff 3 510 8 8 +buttons/OpenFMRadioRedstoneOn 12 510 8 8 +buttons/OpenFMRadioStartOff 178 305 24 24 +buttons/OpenFMRadioStartOn 203 305 24 24 +buttons/OpenFMRadioStopOff 228 305 24 24 +buttons/OpenFMRadioStopOn 253 305 24 24 +buttons/OpenFMRadioVolumeDownOff 9 484 10 10 +buttons/OpenFMRadioVolumeDownOn 20 484 10 10 +buttons/OpenFMRadioVolumeUpOff 31 484 10 10 +buttons/OpenFMRadioVolumeUpOn 42 484 10 10 +buttons/PowerOff 313 330 18 18 +buttons/PowerOn 332 330 18 18 +icons/Antenna 238 381 16 16 +icons/ArrowRight 255 381 16 16 +icons/AspectRatio 272 381 16 16 +icons/ButtonCheck 153 363 17 17 +icons/ButtonClipboard 171 363 17 17 +icons/ButtonRandomize 189 363 17 17 +icons/CPU 289 381 16 16 +icons/Card 306 381 16 16 +icons/Close 0 451 15 14 +icons/Code 323 381 16 16 +icons/ComponentBus 340 381 16 16 +icons/Copy 357 381 16 16 +icons/Cross 374 381 16 16 +icons/Delete 391 381 16 16 +icons/DragLMB 351 330 21 14 +icons/DragRMB 373 330 21 14 +icons/EEPROM 408 381 16 16 +icons/Edit 425 381 16 16 +icons/Eject 442 381 16 16 +icons/File 459 381 16 16 +icons/Floppy 476 381 16 16 +icons/Folder 493 381 16 16 +icons/FolderSlash 510 381 16 16 +icons/Grid 187 330 22 22 +icons/GridOff 210 330 22 22 +icons/HDD 527 381 16 16 +icons/Home 233 330 22 22 +icons/LMB 71 466 11 14 +icons/Label 544 381 16 16 +icons/LinesHorizontal 561 381 16 16 +icons/Link 578 381 16 16 +icons/LinkSlash 595 381 16 16 +icons/Memory 612 381 16 16 +icons/Microchip 629 381 16 16 +icons/NA 646 381 16 16 +icons/NotificationError 95 466 11 11 +icons/NotificationInfo 107 466 11 11 +icons/NotificationWarning 119 466 11 11 +icons/Pin 26 466 14 14 +icons/Plus 663 381 16 16 +icons/Power 680 381 16 16 +icons/RMB 83 466 11 14 +icons/Restart 697 381 16 16 +icons/Save 714 381 16 16 +icons/SaveAs 731 381 16 16 +icons/Server 748 381 16 16 +icons/SettingsSound 0 466 12 17 +icons/SettingsUI 13 466 12 17 +icons/Tier0 765 381 16 16 +icons/Tier1 782 381 16 16 +icons/Tier2 799 381 16 16 +icons/Tiers 816 381 16 16 +icons/Unpin 41 466 14 14 +icons/WaveLFSR 121 434 24 10 +icons/WaveNoise 146 434 24 10 +icons/WaveSawtooth 171 434 24 10 +icons/WaveSine 196 434 24 10 +icons/WaveSquare 221 434 24 10 +icons/WaveTriangle 246 434 24 10 +icons/Window 833 381 16 16 +icons/WireArrowLeft 2 538 4 8 +icons/WireArrowRight 7 538 4 8 +items/APU0 85 305 16 96 +items/APU1 102 305 16 96 +items/APU2 119 305 16 96 +items/CPU0 850 381 16 16 +items/CPU1 867 381 16 16 +items/CPU2 884 381 16 16 +items/CardBase 901 381 16 16 +items/CircuitBoard 918 381 16 16 +items/ComponentBus0 935 381 16 16 +items/ComponentBus1 952 381 16 16 +items/ComponentBus2 969 381 16 16 +items/ComponentBus3 986 381 16 16 +items/DataCard0 0 305 16 128 +items/DataCard1 17 305 16 128 +items/DataCard2 34 305 16 128 +items/DebugCard 1003 381 16 16 +items/DiskDriveMountable 278 305 16 16 +items/EEPROM 295 305 16 16 +items/FloppyDisk_dyeBlack 312 305 16 16 +items/FloppyDisk_dyeBlue 329 305 16 16 +items/FloppyDisk_dyeBrown 346 305 16 16 +items/FloppyDisk_dyeCyan 363 305 16 16 +items/FloppyDisk_dyeGray 380 305 16 16 +items/FloppyDisk_dyeGreen 397 305 16 16 +items/FloppyDisk_dyeLightBlue 414 305 16 16 +items/FloppyDisk_dyeLightGray 431 305 16 16 +items/FloppyDisk_dyeLime 448 305 16 16 +items/FloppyDisk_dyeMagenta 465 305 16 16 +items/FloppyDisk_dyeOrange 482 305 16 16 +items/FloppyDisk_dyePink 499 305 16 16 +items/FloppyDisk_dyePurple 516 305 16 16 +items/FloppyDisk_dyeRed 533 305 16 16 +items/FloppyDisk_dyeWhite 550 305 16 16 +items/FloppyDisk_dyeYellow 567 305 16 16 +items/GraphicsCard0 584 305 16 16 +items/GraphicsCard1 601 305 16 16 +items/GraphicsCard2 618 305 16 16 +items/HardDiskDrive0 635 305 16 16 +items/HardDiskDrive1 652 305 16 16 +items/HardDiskDrive2 669 305 16 16 +items/InternetCard 153 330 16 32 +items/LinkedCard 136 305 16 96 +items/Memory0 686 305 16 16 +items/Memory1 703 305 16 16 +items/Memory2 720 305 16 16 +items/Memory3 737 305 16 16 +items/Memory4 754 305 16 16 +items/Memory5 771 305 16 16 +items/NetworkCard 788 305 16 16 +items/OcelotCard 51 305 16 128 +items/RedstoneCard0 805 305 16 16 +items/RedstoneCard1 822 305 16 16 +items/SelfDestructingCard 170 330 16 32 +items/Server0 839 305 16 16 +items/Server1 856 305 16 16 +items/Server2 873 305 16 16 +items/Server3 890 305 16 16 +items/SoundCard 68 305 16 128 +items/WirelessNetworkCard0 907 305 16 16 +items/WirelessNetworkCard1 924 305 16 16 +light-panel/BookmarkLeft 102 434 18 14 +light-panel/BookmarkRight 207 363 20 14 +light-panel/BorderB 13 547 4 4 +light-panel/BorderL 103 547 4 2 +light-panel/BorderR 18 547 4 4 +light-panel/BorderT 23 547 4 4 +light-panel/CornerBL 28 547 4 4 +light-panel/CornerBR 33 547 4 4 +light-panel/CornerTL 38 547 4 4 +light-panel/CornerTR 43 547 4 4 +light-panel/Fill 6 558 2 2 +light-panel/Vent 0 499 2 38 +nodes/Cable 21 510 8 8 +nodes/Camera 941 305 16 16 +nodes/Chest 56 466 14 14 +nodes/HologramProjector0 958 305 16 16 +nodes/HologramProjector1 975 305 16 16 +nodes/IronNoteBlock 992 305 16 16 +nodes/Lamp 85 402 16 16 +nodes/LampFrame 102 402 16 16 +nodes/LampGlow 305 106 128 128 +nodes/NewNode 119 402 16 16 +nodes/NoteBlock 136 402 16 16 +nodes/OpenFMRadio 153 402 16 16 +nodes/Relay 170 402 16 16 +nodes/computer/Activity 187 402 16 16 +nodes/computer/Default 204 402 16 16 +nodes/computer/Error 221 402 16 16 +nodes/computer/On 238 402 16 16 +nodes/disk-drive/Activity 255 402 16 16 +nodes/disk-drive/Default 272 402 16 16 +nodes/disk-drive/Floppy 289 402 16 16 +nodes/rack/Default 306 402 16 16 +nodes/rack/Empty 323 402 16 16 +nodes/rack/drive/0/Activity 340 402 16 16 +nodes/rack/drive/0/Default 357 402 16 16 +nodes/rack/drive/0/Floppy 374 402 16 16 +nodes/rack/drive/1/Activity 391 402 16 16 +nodes/rack/drive/1/Default 408 402 16 16 +nodes/rack/drive/1/Floppy 425 402 16 16 +nodes/rack/drive/2/Activity 442 402 16 16 +nodes/rack/drive/2/Default 459 402 16 16 +nodes/rack/drive/2/Floppy 476 402 16 16 +nodes/rack/drive/3/Activity 493 402 16 16 +nodes/rack/drive/3/Default 510 402 16 16 +nodes/rack/drive/3/Floppy 527 402 16 16 +nodes/rack/drive/Floppy 544 402 16 16 +nodes/rack/server/0/Activity 561 402 16 16 +nodes/rack/server/0/Default 578 402 16 16 +nodes/rack/server/0/Error 595 402 16 16 +nodes/rack/server/0/On 612 402 16 16 +nodes/rack/server/1/Activity 629 402 16 16 +nodes/rack/server/1/Default 646 402 16 16 +nodes/rack/server/1/Error 663 402 16 16 +nodes/rack/server/1/On 680 402 16 16 +nodes/rack/server/2/Activity 697 402 16 16 +nodes/rack/server/2/Default 714 402 16 16 +nodes/rack/server/2/Error 731 402 16 16 +nodes/rack/server/2/On 748 402 16 16 +nodes/rack/server/3/Activity 765 402 16 16 +nodes/rack/server/3/Default 782 402 16 16 +nodes/rack/server/3/Error 799 402 16 16 +nodes/rack/server/3/On 816 402 16 16 +nodes/screen/BottomLeft 833 402 16 16 +nodes/screen/BottomMiddle 850 402 16 16 +nodes/screen/BottomRight 867 402 16 16 +nodes/screen/ColumnBottom 884 402 16 16 +nodes/screen/ColumnMiddle 901 402 16 16 +nodes/screen/ColumnTop 918 402 16 16 +nodes/screen/Middle 935 402 16 16 +nodes/screen/MiddleLeft 952 402 16 16 +nodes/screen/MiddleRight 969 402 16 16 +nodes/screen/PowerOnOverlay 986 402 16 16 +nodes/screen/RowLeft 1003 402 16 16 +nodes/screen/RowMiddle 0 434 16 16 +nodes/screen/RowRight 17 434 16 16 +nodes/screen/Standalone 34 434 16 16 +nodes/screen/TopLeft 51 434 16 16 +nodes/screen/TopMiddle 68 434 16 16 +nodes/screen/TopRight 85 434 16 16 +panel/BorderB 48 547 4 4 +panel/BorderL 108 547 4 2 +panel/BorderR 53 547 4 4 +panel/BorderT 58 547 4 4 +panel/CornerBL 63 547 4 4 +panel/CornerBR 68 547 4 4 +panel/CornerTL 73 547 4 4 +panel/CornerTR 78 547 4 4 +panel/Fill 9 558 2 2 +particles/Note 21 499 7 10 +screen/BorderB 5 547 2 8 +screen/BorderT 2 547 2 10 +screen/CornerBL 30 510 8 8 +screen/CornerBR 39 510 8 8 +screen/CornerTL 3 499 8 10 +screen/CornerTR 12 499 8 10 +window/BorderDark 2 558 1 4 +window/BorderLight 4 558 1 4 +window/CornerBL 83 547 4 4 +window/CornerBR 88 547 4 4 +window/CornerTL 93 547 4 4 +window/CornerTR 98 547 4 4 +window/OpenFMRadio 305 0 232 105 +window/case/Motherboard 612 0 79 70 +window/rack/Lines 538 0 73 91 +window/rack/Motherboard 434 106 100 78 +window/rack/NetworkBack 20 552 1 2 +window/rack/NetworkBottom 22 552 1 2 +window/rack/NetworkConnector 24 552 1 2 +window/rack/NetworkLeft 26 552 1 2 +window/rack/NetworkRight 28 552 1 2 +window/rack/NetworkTop 30 552 1 2 +window/rack/NodeBack 113 547 5 1 +window/rack/NodeBottom 119 547 5 1 +window/rack/NodeLeft 125 547 5 1 +window/rack/NodeRight 131 547 5 1 +window/rack/NodeTop 137 547 5 1 +window/rack/SideBack 8 552 1 3 +window/rack/SideBottom 10 552 1 3 +window/rack/SideConnector 12 552 1 3 +window/rack/SideLeft 14 552 1 3 +window/rack/SideRight 16 552 1 3 +window/rack/SideTop 18 552 1 3 diff --git a/src/main/scala/ocelot/desktop/entity/OcelotCard.scala b/src/main/scala/ocelot/desktop/entity/OcelotCard.scala new file mode 100644 index 0000000..9b100e3 --- /dev/null +++ b/src/main/scala/ocelot/desktop/entity/OcelotCard.scala @@ -0,0 +1,61 @@ +package ocelot.desktop.entity + +import ocelot.desktop.entity.OcelotCard.LogEvent +import totoro.ocelot.brain.Constants +import totoro.ocelot.brain.entity.machine.{Arguments, Callback, Context} +import totoro.ocelot.brain.entity.traits.DeviceInfo.{DeviceAttribute, DeviceClass} +import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, Environment, Tiered, result} +import totoro.ocelot.brain.event.{EventBus, NodeEvent} +import totoro.ocelot.brain.network.{Network, Node, Visibility} +import totoro.ocelot.brain.util.Tier +import totoro.ocelot.brain.util.Tier.Tier + +import java.time.Instant + +class OcelotCard extends Entity with Environment with DeviceInfo with Tiered { + override val node: Node = Network.newNode(this, Visibility.Neighbors) + .withComponent("ocelot", Visibility.Neighbors) + .create() + + override def tier: Tier = Tier.One + + private final lazy val deviceInfo = Map( + DeviceAttribute.Class -> DeviceClass.Generic, + DeviceAttribute.Description -> "Yarn ball manipulator", + DeviceAttribute.Vendor -> Constants.DeviceInfo.DefaultVendor, + DeviceAttribute.Product -> "Catbus", + ) + + override def getDeviceInfo: Map[String, String] = deviceInfo + + @Callback(direct = true, doc = """function(message: string) -- Logs a message to the Ocelot console.""") + def log(context: Context, args: Arguments): Array[AnyRef] = { + val message = args.checkString(0) + EventBus.send(LogEvent(node.address, message)) + result() + } + + @Callback(direct = true, doc = """function(): integer -- Returns the current Unix timestamp (UTC, in milliseconds).""") + def getTimestamp(context: Context, args: Arguments): Array[AnyRef] = { + result(Instant.now().toEpochMilli) + } + + @Callback(direct = true, doc = """function(): integer -- Returns a high-resolution timer value (in nanoseconds).""") + def getInstant(context: Context, args: Arguments): Array[AnyRef] = { + result(System.nanoTime()) + } + + @Callback(direct = true, doc = """function(): number -- Returns the remaining call budget.""") + def getRemainingCallBudget(context: Context, args: Arguments): Array[AnyRef] = { + result(context.getRemainingCallBudget) + } + + @Callback(direct = true, doc = """function(): number -- Returns the maximum call budget.""") + def getMaxCallBudget(context: Context, args: Arguments): Array[AnyRef] = { + result(context.getMaxCallBudget) + } +} + +object OcelotCard { + case class LogEvent(address: String, message: String) extends NodeEvent +} diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index 0784d13..4362f8b 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -220,7 +220,7 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { def text(x: Float, y: Float, text: String, shrink: Int = 0): Unit = { var ox = x - for (c <- text) { + text.codePoints().forEach { c => char(ox, y, c) ox += _font.charWidth(c) - shrink } @@ -329,7 +329,7 @@ class Graphics(private var scalingFactor: Float) extends Logging with Resource { val uvTransform = Transform2D.translate(spriteRect.x, spriteRect.y) >> (if (fixUV) - Transform2D.scale(spriteRect.w - 0.25f / 512, spriteRect.h - 0.25f / 512) + Transform2D.scale(spriteRect.w - 0.25f / 1024, spriteRect.h - 0.25f / 1024) else Transform2D.scale(spriteRect.w, spriteRect.h)) diff --git a/src/main/scala/ocelot/desktop/graphics/IconSource.scala b/src/main/scala/ocelot/desktop/graphics/IconSource.scala index 02828e9..bebdbe4 100644 --- a/src/main/scala/ocelot/desktop/graphics/IconSource.scala +++ b/src/main/scala/ocelot/desktop/graphics/IconSource.scala @@ -60,6 +60,8 @@ object IconSource { val SelfDestructingCard: IconSource = IconSource("items/SelfDestructingCard", animation = Some(Animations.SelfDestructingCard)) + val OcelotCard: IconSource = IconSource("items/OcelotCard", animation = Some(Animations.OcelotCard)) + val HardDiskDrive: Tier => IconSource = { tier => IconSource(s"items/HardDiskDrive${tier.id}") } @@ -101,6 +103,9 @@ object IconSource { (0, 4f), (1, 4f), (2, 4f), (3, 4f), (4, 4f), (5, 4f), (6, 4f), (7, 4f)) val SelfDestructingCard: Animation = Array((0, 4f), (1, 4f)) + + val OcelotCard: Animation = Array( + (0, 12f), (1, 4f), (2, 4f), (3, 4f), (4, 5f), (5, 6f), (0, 9f), (6, 4f), (7, 3f)) } // ----------------------- Ocelot interface icons ----------------------- diff --git a/src/main/scala/ocelot/desktop/inventory/Items.scala b/src/main/scala/ocelot/desktop/inventory/Items.scala index 85c9e82..b49008d 100644 --- a/src/main/scala/ocelot/desktop/inventory/Items.scala +++ b/src/main/scala/ocelot/desktop/inventory/Items.scala @@ -139,6 +139,7 @@ object Items extends Logging { } registerSingleton(SoundCardItem.Factory) registerSingleton(SelfDestructingCardItem.Factory) + registerSingleton(OcelotCardItem.Factory) registerTiered("Server", Tier.One to Tier.Creative)(new ServerItem.Factory(_)) registerTiered("Component bus", Tier.One to Tier.Creative)(new ComponentBusItem.Factory(_)) diff --git a/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala new file mode 100644 index 0000000..3f8dfee --- /dev/null +++ b/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala @@ -0,0 +1,104 @@ +package ocelot.desktop.inventory.item + +import ocelot.desktop.ColorScheme +import ocelot.desktop.color.Color +import ocelot.desktop.entity.OcelotCard +import ocelot.desktop.graphics.IconSource +import ocelot.desktop.inventory.item.OcelotCardItem.MessagesTag +import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem} +import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} +import ocelot.desktop.ui.event.BrainEvent +import ocelot.desktop.ui.widget.card.OcelotCardWindow +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} +import ocelot.desktop.ui.widget.window.Windowed +import totoro.ocelot.brain.nbt.ExtendedNBT.extendNBTTagList +import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound, NBTTagString} +import totoro.ocelot.brain.util.Tier +import totoro.ocelot.brain.util.Tier.Tier + +import scala.collection.IndexedSeqView +import scala.collection.mutable.ArrayBuffer +import scala.jdk.CollectionConverters.BufferHasAsJava + +class OcelotCardItem(val ocelotCard: OcelotCard) + extends Item + with ComponentItem + with PersistableItem + with CardItem + with Windowed[OcelotCardWindow] { + + private val _messages: ArrayBuffer[String] = ArrayBuffer.empty + + def messages: IndexedSeqView[String] = _messages.view + + eventHandlers += { + case BrainEvent(OcelotCard.LogEvent(_, message)) => + addMessage(message) + } + + override def createWindow(): OcelotCardWindow = new OcelotCardWindow(this) + + override def component: OcelotCard = ocelotCard + + override def tooltipNameColor: Color = ColorScheme("OcelotCardTooltip") + + override def fillRmbMenu(menu: ContextMenu): Unit = { + menu.addEntry(ContextMenuEntry("Open console", IconSource.Window) { + window.open() + }) + + menu.addSeparator() + + super.fillRmbMenu(menu) + } + + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + + clear() + addMessages(nbt.getTagList(MessagesTag, NBT.TAG_STRING).iterator[NBTTagString].map(_.getString)) + } + + override def save(nbt: NBTTagCompound): Unit = { + super.save(nbt) + nbt.setTagList(MessagesTag, _messages.map(new NBTTagString(_).asInstanceOf[NBTBase]).asJava) + } + + override def factory: ItemFactory = OcelotCardItem.Factory + + def clear(): Unit = { + _messages.clear() + window.onMessagesCleared() + } + + private def addMessage(message: String): Unit = { + _messages += message + window.onMessagesAdded(1) + } + + private def addMessages(messages: Iterator[String]): Unit = { + val currentSize = this._messages.length + this._messages ++= messages + window.onMessagesAdded(this._messages.length - currentSize) + } +} + +object OcelotCardItem { + val MessagesTag = "messages" + + object Factory extends ItemFactory { + override type I = OcelotCardItem + + override def itemClass: Class[I] = classOf + + override def name: String = "Ocelot Card" + + override def tier: Option[Tier] = Some(Tier.One) + + override def icon: IconSource = IconSource.OcelotCard + + override def build(): OcelotCardItem = new OcelotCardItem(new OcelotCard) + + override def recoverers: Iterable[ItemRecoverer[_, _]] = Nil + } +} diff --git a/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala b/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala index aac1b72..488d485 100644 --- a/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala +++ b/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala @@ -1,28 +1,26 @@ package ocelot.desktop.node -import ocelot.desktop.inventory.{Item, SyncedInventory} +import ocelot.desktop.inventory.SyncedInventory +import ocelot.desktop.inventory.traits.ComponentItem import ocelot.desktop.ui.event.Event import totoro.ocelot.brain.entity.traits.{Entity, Environment} abstract class SyncedInventoryEntityNode(entity: Entity with Environment) extends EntityNode(entity) - with SyncedInventory -{ + with SyncedInventory { + override def shouldReceiveEventsFor(address: String): Boolean = super.shouldReceiveEventsFor(address) || - brainInventory.inventory.entities.exists { - case env: Environment => env.node.address == address - } + inventoryIterator + .flatMap(_.get) + .collect { case item: ComponentItem => item } + .exists(_.component.node.address == address) override def handleEvent(event: Event): Unit = { super.handleEvent(event) - inventoryIterator.foreach(slot => - slot.get match { - case Some(item: Item) => - item.handleEvent(event) - case _ => - } - ) + for (slot <- inventoryIterator; item <- slot.get) { + item.handleEvent(event) + } } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala new file mode 100644 index 0000000..df4ea5a --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -0,0 +1,94 @@ +package ocelot.desktop.ui.widget + +import ocelot.desktop.ColorScheme +import ocelot.desktop.geometry.{Padding2D, Size2D} +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.layout.{Layout, LinearLayout} +import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryPadding} +import ocelot.desktop.util.{DrawUtils, Orientation} + +import scala.collection.immutable.ArraySeq + +abstract class LogWidget extends Widget { + override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) + + private val messageListWidget = new Widget { + override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) + } + + children :+= new PaddingBox( + new ScrollView(messageListWidget), + Padding2D.equal(BorderThickness), + ) + + protected def messages: Iterable[String] + + protected def textWidth: Int + + override def minimumSize: Size2D = Size2D( + textWidth * 8 + + 2 * BorderThickness + // entry border (left + right) + 2 * EntryPadding + + 2 * BorderThickness, // outer border (left + right) + + 16 + + 2 * BorderThickness + // entry border (top + bottom) + 2 * EntryPadding + + 2 * BorderThickness, // outer border (top + bottom) + ) + + def onMessagesAdded(count: Int): Unit = { + for (message <- messages.drop(messages.size - count)) { + messageListWidget.children :+= new Entry(message) + } + + recalculateBoundsAndRelayout() + } + + def onMessagesCleared(): Unit = { + messageListWidget.children = ArraySeq.empty + recalculateBoundsAndRelayout() + } + + override def draw(g: Graphics): Unit = { + g.rect(bounds, ColorScheme("LogBackground")) + DrawUtils.ring(g, bounds.x, bounds.y, bounds.w, bounds.h, BorderThickness, ColorScheme("LogBorder")) + + super.draw(g) + } + + private class Entry(val message: String) extends Widget { + override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) + + children :+= new PaddingBox( + new Text { + override def text: String = message + + override def wrapWidth: Option[Int] = Some(LogWidget.this.textWidth) + }, + Padding2D.equal(EntryPadding + BorderThickness), + ) + + override def draw(g: Graphics): Unit = { + val innerBounds = bounds.inflate(-EntryPadding) + val ringBounds = innerBounds.inflate(BorderThickness) + g.rect(innerBounds, ColorScheme("LogEntryBackground")) + DrawUtils.ring( + g, + ringBounds.x, + ringBounds.y, + ringBounds.w, + ringBounds.h, + BorderThickness, + ColorScheme("LogEntryBorder"), + ) + + super.draw(g) + } + } +} + +object LogWidget { + val BorderThickness = 2 + val EntryPadding = 4 +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/Text.scala b/src/main/scala/ocelot/desktop/ui/widget/Text.scala new file mode 100644 index 0000000..ae10db6 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/Text.scala @@ -0,0 +1,169 @@ +package ocelot.desktop.ui.widget + +import ocelot.desktop.ColorScheme +import ocelot.desktop.color.Color +import ocelot.desktop.geometry.Size2D +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.widget.Text.{CharHeight, getWidth, wrap} +import totoro.ocelot.brain.util.FontUtils + +import scala.collection.mutable.ArrayBuffer + +class Text extends Widget { + def text: String = "" + + private var cachedText = "" + private var cachedWrapWidth = None: Option[Int] + private var cachedLines = ArrayBuffer.empty[String] + private var cachedWidth = 0 + + def color: Color = ColorScheme("Label") + + def wrapWidth: Option[Int] = None + + private def recomputeIfNeeded(): Unit = { + if (text != cachedText || cachedWrapWidth != wrapWidth) { + recomputeProperties() + } + } + + final def textWidth: Int = { + recomputeIfNeeded() + cachedWidth + } + + final def textHeight: Int = { + recomputeIfNeeded() + cachedLines.length + } + + final def textLines: Iterable[String] = { + recomputeIfNeeded() + cachedLines + } + + override def minimumSize: Size2D = Size2D(textWidth, CharHeight * (textHeight max 1)) + override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity) + + override def draw(g: Graphics): Unit = { + g.background = Color.Transparent + g.foreground = color + + for ((line, idx) <- textLines.zipWithIndex) { + g.text(position.x, position.y + idx * CharHeight, line) + } + } + + private def recomputeProperties(): Unit = { + cachedText = text + cachedWrapWidth = wrapWidth + cachedLines = wrap(cachedText, cachedWrapWidth) + cachedWidth = getWidth(cachedLines) + } +} + +object Text { + private val CharHeight = 16 + + private def getWidth(codepoint: Int): Int = FontUtils.wcwidth(codepoint) + + private def getWidth(line: String): Int = + line.codePoints().map(getWidth).sum() + + private def getWidth(lines: Iterable[String]): Int = + lines.iterator.map(getWidth).maxOption.getOrElse(0) + + private def wrap(text: String, wrapWidth: Option[Int]): ArrayBuffer[String] = { + val lines = text.linesIterator + .flatMap(line => wrapWidth match { + case Some(wrapWidth) if getWidth(line) > wrapWidth => wrapLine(line, wrapWidth) + case _ => Some(line) + }) + .to(ArrayBuffer) + + val lastNonEmptyLineIdx = lines.lastIndexWhere(_.nonEmpty) + lines.takeInPlace(lastNonEmptyLineIdx + 1) + + lines + } + + private def wrapLine(line: String, wrapWidth: Int): Iterable[String] = { + // adapted from cheburator's message splitter + + val lines = ArrayBuffer.empty[String] + + var lineStart = 0 + var pos = -1 + var lineWidth = 0 + var lastSpacePos = -1 + var lastSpaceSize = 1 + var spaceSeq = false + + def firstLine: Boolean = lines.isEmpty + + def pushLine(line: String): Unit = { + lines += line + + lastSpacePos = -1 + lastSpaceSize = 1 + lineWidth = 0 + spaceSeq = false + } + + while (pos + 1 < line.length) { + pos += 1 + + val (c, count) = if (pos + 1 < line.length && Character.isSurrogatePair(line(pos), line(pos + 1))) { + (Character.toCodePoint(line(pos), line(pos + 1)), 2) + } else (line(pos): Int, 1) + + val isSpace = Character.isSpaceChar(c) + + if (isSpace && pos == lineStart && !firstLine) { + // skip leading space + lineStart += count + } else { + if (!isSpace) { + spaceSeq = false + } + + if (isSpace && !spaceSeq) { + lastSpacePos = pos + lastSpaceSize = count + spaceSeq = true + } + + lineWidth += getWidth(c) + + if (lineWidth > wrapWidth) { + if (lastSpacePos >= (pos - lineStart) / 2) { + val newLine = line.substring(lineStart, lastSpacePos) + + // `+ lastSpaceSize` starts the line after the last whitespace character + // we set pos to the same position -- but before it gets there, it'll get incremented by `count`, + // so we do the subtraction + lineStart = lastSpacePos + lastSpaceSize + pos = lastSpacePos + lastSpaceSize - count + + pushLine(newLine) + } else { + val newLine = line.substring(lineStart, pos) + lineStart = pos + pos -= 1 + + pushLine(newLine) + } + } + } + + // subtract one, which we'll add back at the start of the next iteration + pos += count - 1 + } + + if (lineStart != pos + 1) { + pushLine(line.substring(lineStart)) + } + + lines + } +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/card/OcelotCardWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/card/OcelotCardWindow.scala new file mode 100644 index 0000000..fc053d5 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/card/OcelotCardWindow.scala @@ -0,0 +1,50 @@ +package ocelot.desktop.ui.widget.card + +import ocelot.desktop.geometry.{Padding2D, Size2D} +import ocelot.desktop.inventory.item.OcelotCardItem +import ocelot.desktop.ui.layout.{Layout, LinearLayout} +import ocelot.desktop.ui.widget.window.PanelWindow +import ocelot.desktop.ui.widget.{Button, Filler, LogWidget, PaddingBox, Widget} +import ocelot.desktop.util.Orientation + +class OcelotCardWindow(item: OcelotCardItem) extends PanelWindow { + override protected def title: String = s"Ocelot card ${item.component.node.address}" + + private val logWidget: LogWidget = new LogWidget { + override protected def messages: Iterable[String] = item.messages + override protected def textWidth: Int = 50 + + override def minimumSize: Size2D = super.minimumSize.copy(height = 300) + override def maximumSize: Size2D = minimumSize + } + + setInner(new Widget { + override val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) + + children :+= logWidget + + children :+= new PaddingBox( + new Widget { + children :+= new Filler + + children :+= new Button { + override def text: String = "Clear" + override def onClick(): Unit = clear() + } + }, + Padding2D(top = 4) + ) + }) + + private def clear(): Unit = { + item.clear() + } + + def onMessagesAdded(count: Int): Unit = { + logWidget.onMessagesAdded(count) + } + + def onMessagesCleared(): Unit = { + logWidget.onMessagesCleared() + } +}