Skip to content

Commit 0cbe127

Browse files
0.30
1 parent 7538000 commit 0cbe127

34 files changed

+427
-118
lines changed

.github/workflows/wheels.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,28 @@ jobs:
1919
os: [ubuntu-22.04, windows-2022, macos-11]
2020

2121
steps:
22-
- uses: actions/checkout@v3
22+
- uses: actions/checkout@v4
2323

2424
- name: Build wheels
25-
uses: pypa/cibuildwheel@v2.14.1
25+
uses: pypa/cibuildwheel@v2.16.5
2626

27-
- uses: actions/upload-artifact@v3
27+
- uses: actions/upload-artifact@v4
2828
with:
29+
name: dist-${{ matrix.os }}
2930
path: ./wheelhouse/*.whl
3031

3132
build_sdist:
3233
name: Build source distribution
3334
runs-on: ubuntu-latest
3435
steps:
35-
- uses: actions/checkout@v3
36+
- uses: actions/checkout@v4
3637

3738
- name: Build sdist
3839
run: pipx run build --sdist
3940

40-
- uses: actions/upload-artifact@v3
41+
- uses: actions/upload-artifact@v4
4142
with:
43+
name: dist-${{ matrix.os }}
4244
path: dist/*.tar.gz
4345

4446
upload_pypi:
@@ -49,9 +51,10 @@ jobs:
4951
id-token: write
5052
if: github.event_name == 'release' && github.event.action == 'published'
5153
steps:
52-
- uses: actions/download-artifact@v3
54+
- uses: actions/download-artifact@v4
5355
with:
54-
name: artifact
56+
pattern: dist-*
57+
merge-multiple: true
5558
path: dist
5659

5760
- uses: pypa/gh-action-pypi-publish@release/v1

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ install:
1212
CYTHONIZE=1 pip install .
1313

1414
install-from-source: dist
15-
pip install dist/minecraft-python-0.29.tar.gz
15+
pip install dist/minecraft-python-0.30.tar.gz
1616

1717
clean:
1818
$(RM) -r build dist src/*.egg-info

README.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
## Minecraft: Python Edition
22

3-
![Minecraft](/screenshot.png?raw=true)
3+
![Minecraft](/creative.png?raw=true)
44

55
_**Minecraft: Python Edition**_ is a project that strives to recreate each and every old Minecraft version in Python 3 using the **Pyglet** multimedia library and **Cython** for performance.
66

7-
This project is currently recreating the **Late Classic** versions of Minecraft. The latest version is **Classic 0.29_02** as released on _**October 30, 2009**_.
7+
This project has finished recreating the **Classic** versions of Minecraft. The latest version is **Classic 0.30** as released on _**November 10, 2009**_.
88

9-
Learn more about this version [here](https://minecraft.fandom.com/wiki/Java_Edition_Classic_0.29_02).
9+
This version comes with both survival and creative modes: to enable survival mode, just run with the argument `-survival`.
1010

11-
Or the server version [here](https://minecraft.fandom.com/wiki/Java_Edition_Classic_server_1.8.3).
11+
Learn more about this version [here](https://minecraft.wiki/w/Java_Edition_Classic_0.30).
12+
13+
Or the server version [here](https://minecraft.wiki/w/Java_Edition_Classic_server_1.10.1).
1214

1315
This project is organized so that every commit is strictly the finished Python version of the Java game of the same version number.
1416
This means that you can go back into this repository's commit history and see only the source code changes between versions of Minecraft,
@@ -18,34 +20,40 @@ you can play it just by specifying the Minecraft version you want to play in the
1820
### General Usage
1921

2022
*Pyglet*, *Cython*, *Pillow*, and *PyOgg* are required dependencies and can easily be installed with *pip*. Use the versions specified in `requirements.txt`.
23+
*wxPython* is an optional dependency for the level file picker, but if you don't have it then *Tkinter* is used instead.
2124

2225
For audio to work you will either need *PyOgg* which is recommended, or FFmpeg which is installed on the system.
2326
GStreamer is also supported on Linux through the *gst-python* library. PyOgg requires that your system have one of the *Opus*, *FLAC*, or *Vorbis* codecs. OpenAL is required.
2427

25-
To easily install this version of *Minecraft: Python Edition*, just run `python -m pip install minecraft-python==0.29`.
28+
To easily install this version of *Minecraft: Python Edition*, just run `python -m pip install minecraft-python==0.30`.
2629

2730
Alternatively, for a manual Cython build, run `python setup.py build_ext --inplace`.
2831

2932
Run `python -m mc.net.minecraft.Minecraft` to launch the game. *Minecraft: Python Edition* should be compatible with any modern platform that supports OpenGL and Cython.
3033

3134
Run with the argument `-fullscreen` to open the window in fullscreen mode.
3235

33-
It is possible to enable a limited survival mode by editing `self.gamemode` in `Minecraft.py`.
36+
### Creative Gameplay
37+
38+
In creative mode, no mobs exist. All Classic ores and tiles are made available by pressing B to pick blocks.
39+
40+
You can press F5 to toggle rain. Other keys are listed in the regular options menu.
41+
42+
### Survival Gameplay
3443

35-
### Gameplay
44+
![Minecraft](/survival.png?raw=true)
3645

37-
This is a creative version of Classic, so no mobs exist. All ores and tiles are featured in this version.
46+
The survival version features early mobs (pigs, creepers, skeletons, zombies, spiders) and basic combat. Press Tab to launch arrows at enemies.
3847

39-
If you enable survival mode, there will be limited functionality.
40-
Only sheep will spawn, which you can get wool from. Apart from that, no items drop.
48+
There are pigs that drop brown mushrooms. Sheep also exist. Creepers explode only upon death.
4149

42-
Press B to pick blocks. Press F5 to toggle rain. Other keys are listed in the regular options menu.
50+
To heal, pick up mushrooms and right click to eat. Red mushrooms are poisonous and will take away health.
4351

4452
### Multiplayer
4553

46-
To launch the multiplayer game, run `python -m mc.net.minecraft.Minecraft -server <host:port> -user <username> -mppass [password]`.
54+
To launch the multiplayer game while in creative mode, run `python -m mc.net.minecraft.Minecraft -server <host:port> -user <username> -mppass [password]`.
4755

48-
This client is compatible with any 0.30 server that doesn't use an extended network protocol.
56+
This client is compatible with any Classic server that doesn't use an extended protocol.
4957

5058
Press *Tab* in-game to view connected players.
5159

creative.png

425 KB
Loading

mc/net/minecraft/LevelLoaderListener.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from mc.CompatibilityShims import getMillis
12
from mc.net.minecraft import StopGameException
23
from mc.net.minecraft.renderer.Tesselator import tesselator
34
from pyglet import clock, gl
@@ -8,6 +9,7 @@ def __init__(self, minecraft):
89
self.__minecraft = minecraft
910
self.__title = ''
1011
self.__desc = ''
12+
self.__time = getMillis()
1113

1214
def beginLevelLoading(self, desc):
1315
if not self.__minecraft.running:
@@ -36,24 +38,30 @@ def setLoadingProgress(self):
3638
if not self.__minecraft.running:
3739
raise StopGameException
3840
else:
39-
screenWidth = self.__minecraft.width * 240 // self.__minecraft.height
40-
screenHeight = self.__minecraft.height * 240 // self.__minecraft.height
41-
42-
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
43-
44-
t = tesselator
45-
id_ = self.__minecraft.textures.loadTexture('dirt.png')
46-
gl.glBindTexture(gl.GL_TEXTURE_2D, id_)
47-
s = 32.0
48-
t.begin()
49-
t.color(0x404040)
50-
t.vertexUV(0.0, screenHeight, 0.0, 0.0, screenHeight / s)
51-
t.vertexUV(screenWidth, screenHeight, 0.0, screenWidth / s, screenHeight / s)
52-
t.vertexUV(screenWidth, 0.0, 0.0, screenWidth / s, 0.0)
53-
t.vertexUV(0.0, 0.0, 0.0, 0.0, 0.0)
54-
t.end()
55-
56-
self.__minecraft.font.drawShadow(self.__desc, (screenWidth - self.__minecraft.font.width(self.__desc)) // 2, screenHeight // 2 - 4 - 16, 0xFFFFFF)
57-
self.__minecraft.font.drawShadow(self.__title, (screenWidth - self.__minecraft.font.width(self.__title)) // 2, screenHeight // 2 - 4 + 8, 0xFFFFFF)
58-
clock.tick()
59-
self.__minecraft.flip()
41+
time = getMillis()
42+
if time - self.__time < 0 or time - self.__time >= 20:
43+
screenWidth = self.__minecraft.width * 240 // self.__minecraft.height
44+
screenHeight = self.__minecraft.height * 240 // self.__minecraft.height
45+
46+
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
47+
48+
t = tesselator
49+
id_ = self.__minecraft.textures.loadTexture('dirt.png')
50+
gl.glBindTexture(gl.GL_TEXTURE_2D, id_)
51+
s = 32.0
52+
t.begin()
53+
t.color(0x404040)
54+
t.vertexUV(0.0, screenHeight, 0.0, 0.0, screenHeight / s)
55+
t.vertexUV(screenWidth, screenHeight, 0.0, screenWidth / s, screenHeight / s)
56+
t.vertexUV(screenWidth, 0.0, 0.0, screenWidth / s, 0.0)
57+
t.vertexUV(0.0, 0.0, 0.0, 0.0, 0.0)
58+
t.end()
59+
60+
self.__minecraft.font.drawShadow(self.__desc,
61+
(screenWidth - self.__minecraft.font.width(self.__desc)) // 2,
62+
screenHeight // 2 - 4 - 16, 0xFFFFFF)
63+
self.__minecraft.font.drawShadow(self.__title,
64+
(screenWidth - self.__minecraft.font.width(self.__title)) // 2,
65+
screenHeight // 2 - 4 + 8, 0xFFFFFF)
66+
clock.tick()
67+
self.__minecraft.flip()

mc/net/minecraft/Minecraft.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
GL_DEBUG = False
6565

6666
class Minecraft(window.Window):
67-
VERSION_STRING = '0.29_02'
67+
VERSION_STRING = '0.30'
6868
level = None
6969
levelRenderer = None
7070
player = None
@@ -87,12 +87,16 @@ class Minecraft(window.Window):
8787

8888
guiScreenChanged = False
8989

90-
def __init__(self, fullscreen, *args, **kwargs):
90+
def __init__(self, fullscreen, survival, *args, **kwargs):
9191
super().__init__(*args, **kwargs)
9292

9393
self.__fullscreen = fullscreen
9494

95-
self.gamemode = CreativeGameMode(self)
95+
if survival:
96+
self.gamemode = SurvivalGameMode(self)
97+
else:
98+
self.gamemode = CreativeGameMode(self)
99+
96100
self.__timer = Timer(20.0)
97101
self.user = None
98102
self.guiScreen = None
@@ -207,6 +211,8 @@ def on_mouse_press(self, x, y, button, modifiers):
207211
tile = tiles.dirt.id
208212
elif tile == tiles.slabFull.id:
209213
tile = tiles.slabHalf.id
214+
elif tile == tiles.unbreakable.id:
215+
tile = tiles.rock.id
210216

211217
self.player.inventory.grabTexture(tile, isinstance(self.gamemode, CreativeGameMode))
212218
except Exception as e:
@@ -788,7 +794,7 @@ def __pick(self, alpha):
788794
vec2 = rotVec.add(xy * d, x2 * d, y1 * d)
789795
self.hitResult = self.level.clip(rotVec, vec2)
790796
if self.hitResult:
791-
d = self.hitResult.vec.distanceTo(rotVec)
797+
d = self.hitResult.vec.distanceTo(self.gameRenderer.getPlayerRotVec(alpha))
792798

793799
vec = self.gameRenderer.getPlayerRotVec(alpha)
794800
if isinstance(self.gamemode, CreativeGameMode):
@@ -1067,6 +1073,7 @@ def loadLevel(self, level):
10671073
port = None
10681074
name = 'guest'
10691075
mpPass = ''
1076+
survival = False
10701077
for i, arg in enumerate(sys.argv):
10711078
if arg == '-fullscreen':
10721079
fullScreen = True
@@ -1076,8 +1083,10 @@ def loadLevel(self, level):
10761083
name = sys.argv[i + 1]
10771084
elif arg == '-mppass':
10781085
mpPass = sys.argv[i + 1]
1086+
elif arg == '-survival':
1087+
survival = True
10791088

1080-
game = Minecraft(fullScreen, width=854, height=480, caption='Minecraft')
1089+
game = Minecraft(fullScreen, survival, width=854, height=480, caption='Minecraft')
10811090
game.user = User(name, 0, mpPass)
10821091
if server and port:
10831092
game.setServer(server, int(port))

mc/net/minecraft/gamemode/SurvivalGameMode.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class SurvivalGameMode(GameMode):
77

88
def __init__(self, minecraft):
99
super().__init__(minecraft)
10-
self.__x = 0
11-
self.__y = 0
12-
self.__z = 0
10+
self.__x = -1
11+
self.__y = -1
12+
self.__z = -1
1313
self.__oDestroyProgress = 0
1414
self.__destroyProgress = 0
1515
self.__delay = 0
Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
11
from mc.net.minecraft.gui.Screen import Screen
22
from mc.net.minecraft.gui.Button import Button
33

4+
try:
5+
import wx
6+
except:
7+
import tkinter as tk
8+
from tkinter.filedialog import askopenfilename, asksaveasfilename
9+
10+
import gzip
11+
412
class LoadLevelScreen(Screen):
513

614
def __init__(self, screen):
7-
self.__parent = screen
15+
self._parent = screen
816
self.__finished = False
917
self.__loaded = False
1018
self.__levels = []
1119
self.__status = ''
1220
self._title = 'Load level'
13-
self.__unused = False
21+
self.fileLoaded = False
22+
self._isSaveScreen = False
23+
self._levelFile = ''
1424

1525
def run(self):
16-
"""
17-
This is left incomplete as the original code relies on minecraft.net/listmaps.jsp
18-
which no longer exists.
19-
"""
26+
try:
27+
title = 'Save mine file' if self._isSaveScreen else 'Open mine file'
28+
style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT if self._isSaveScreen else wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
29+
app = wx.App(False)
30+
with wx.FileDialog(None, title, wildcard='MINE files (*.mine)|*.mine',
31+
style=style) as fileDialog:
32+
if fileDialog.ShowModal() == wx.ID_CANCEL:
33+
return
2034

21-
self.__status = 'Failed to load levels'
22-
self.__finished = True
35+
self._levelFile = fileDialog.GetPath()
36+
wx.GetApp().ExitMainLoop()
37+
print(self._levelFile)
38+
except NameError:
39+
root = tk.Tk()
40+
root.withdraw()
41+
try:
42+
fileChooser = asksaveasfilename if self._isSaveScreen else askopenfilename
43+
self._levelFile = fileChooser(filetypes=[('Minecraft levels', '*.mine')])
44+
finally:
45+
root.quit()
46+
47+
self.fileLoaded = False
2348

24-
def _setLevels(self, strings):
49+
def _setLevels(self, levels):
2550
for i in range(5):
26-
self._buttons[i].enabled = strings[i] != '-'
27-
self._buttons[i].msg = strings[i]
51+
self._buttons[i].enabled = levels[i] != '-'
52+
self._buttons[i].msg = levels[i]
2853
self._buttons[i].visible = True
2954

3055
def init(self, minecraft, width, height):
@@ -33,20 +58,32 @@ def init(self, minecraft, width, height):
3358
for i in range(5):
3459
self._buttons.append(Button(i, self._width // 2 - 100, self._height // 6 + i * 24, '---'))
3560
self._buttons[i].visible = False
61+
self._buttons[i].enabled = False
3662

3763
self._buttons.append(Button(5, self._width // 2 - 100, self._height // 6 + 120 + 12, 'Load file...'))
3864
self._buttons.append(Button(6, self._width // 2 - 100, self._height // 6 + 168, 'Cancel'))
39-
self._buttons[5].visible = False
4065

41-
self.run()
66+
self.__status = 'Failed to load levels'
67+
self.__finished = True
4268

4369
def _buttonClicked(self, button):
44-
if button.enabled:
45-
if self.__loaded and button.id < 5:
46-
self._loadLevel(button.id)
70+
if self.fileLoaded or not button.enabled:
71+
return
72+
73+
if self.__loaded and button.id < 5:
74+
self._loadLevel(button.id)
75+
elif self.__finished or self.__loaded and button.id == 5:
76+
self.fileLoaded = True
77+
self.run()
78+
elif self.__finished or self.__loaded and button.id == 6:
79+
self._minecraft.setScreen(self._parent)
80+
81+
def _loadFile(self, fileName):
82+
level = self._minecraft.levelIo.load(gzip.open(fileName, 'rb'))
83+
if level:
84+
self._minecraft.loadLevel(level)
4785

48-
if self.__finished or self.__loaded and button.id == 6:
49-
self._minecraft.setScreen(self.__parent)
86+
self._minecraft.setScreen(self._parent)
5087

5188
def _loadLevel(self, id_):
5289
self._minecraft.loadLevel(self._minecraft.user.name, id_)
@@ -55,8 +92,15 @@ def _loadLevel(self, id_):
5592

5693
def render(self, xm, ym):
5794
self._fillGradient(0, 0, self._width, self._height, 1610941696, -1607454624)
58-
self.drawCenteredString(self._font, self._title, self._width / 2, 20, 0xFFFFFF)
59-
if not self.__loaded:
95+
self.drawCenteredString(self._font, self._title, self._width // 2, 20, 0xFFFFFF)
96+
if self.fileLoaded:
97+
self.drawCenteredString(self._font, 'Selecting file..', self._width // 2, self._height // 2 - 4, 0xFFFFFF)
98+
elif not self.__loaded:
6099
self.drawCenteredString(self._font, self.__status, self._width // 2, self._height // 2 - 4, 0xFFFFFF)
100+
super().render(xm, ym)
61101

62-
super().render(xm, ym)
102+
def tick(self):
103+
super().tick()
104+
if self._levelFile:
105+
self._loadFile(self._levelFile)
106+
self._levelFile = ''

0 commit comments

Comments
 (0)