fix(render): 修复白屏/黑屏——视图矩阵、UI着色器、网格构建性能与安装目录
- 相机视图矩阵按 R_cam^T·T(-pos) 正确推导(旧布局非正交,非零视角下场景塌缩为灰屏) - UI 片元着色器改为纹理×颜色(旧实现丢弃纹理 RGB,彩色纹理全白) - UI 纹理与图集 mipmap:图集补齐 2 次幂、UI 上传不生成 mipmap(NPOT 不完整纹理采样纯白) - 网格构建持一次读锁(旧实现每格邻居查询各加锁约 40 万次/区块,出生区块迟迟无网格, 玩家透过脚下的洞看到地下横截面——进游戏黑屏主因);脏区块按距离排序优先构建 - 开启背面剔除(CW 正面):区块边界面向未加载邻居的内侧面不再以暗色出现在视野里 - 出生逻辑:等区块加载后定位地表出生,相机略微俯视 - dev 截图测试:-shot 等待玩家所在 5×5 区块网格就绪再拍(48s 兜底),先测试后打包 - 安装器改为按用户安装到 LOCALAPPDATA(Program Files 无写权限导致启动即崩) - dev 截图工具独立成 devshot_gl.go
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"mc/internal/atlas"
|
||||
"mc/internal/block"
|
||||
"mc/internal/camera"
|
||||
"mc/internal/chunk"
|
||||
"mc/internal/config"
|
||||
"mc/internal/crash"
|
||||
"mc/internal/engine"
|
||||
@@ -41,6 +43,12 @@ import (
|
||||
|
||||
var devMode = flag.Bool("dev", devDefault, "开发模式:直接读取 assets/(默认随构建 tag)")
|
||||
|
||||
// 开发测试参数(dev 测试通过后再打包)
|
||||
var (
|
||||
autoEnter = flag.Bool("auto", false, "自动进入游戏(跳过主菜单点击)")
|
||||
shotPath = flag.String("shot", "", "截图路径:运行约 2 秒后保存 PNG 并退出(dev 测试用)")
|
||||
)
|
||||
|
||||
// clientApp 客户端应用:把会话与渲染粘合起来(engine.Ticker + engine.Renderer)。
|
||||
type clientApp struct {
|
||||
sess *game.Session
|
||||
@@ -70,6 +78,7 @@ type clientApp struct {
|
||||
|
||||
winW, winH int
|
||||
tickCnt uint64
|
||||
frames int // 渲染帧计数(截图时机用)
|
||||
lastTime time.Time
|
||||
}
|
||||
|
||||
@@ -84,6 +93,12 @@ func (a *clientApp) Tick(dt float64) {
|
||||
// 屏幕状态机(UI还原.md §2:主菜单/暂停不处理游戏输入)
|
||||
switch a.screen {
|
||||
case 0: // 主菜单
|
||||
// dev 测试:-auto 自动进入游戏(跳过点击)
|
||||
if *autoEnter {
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
return
|
||||
}
|
||||
if a.in.clicked {
|
||||
switch a.titleScreen.HitTest(a.in.clickX, a.in.clickY) {
|
||||
case "play":
|
||||
@@ -157,22 +172,38 @@ func (a *clientApp) Tick(dt float64) {
|
||||
}
|
||||
}
|
||||
|
||||
// 脏区块网格重建(CPU 侧,预算内)
|
||||
// 脏区块网格重建(CPU 侧,预算内;按距玩家距离排序,先建视野中心——
|
||||
// 随机顺序会致出生区块迟迟无网格,玩家脚下的地面"透明",看到相邻区块地下横截面(黑)。
|
||||
start := time.Now()
|
||||
px, pz := a.sess.Player.Pos[0], a.sess.Player.Pos[2]
|
||||
type dirtyJob struct {
|
||||
c *chunk.Chunk
|
||||
d float64
|
||||
}
|
||||
var dirty []dirtyJob
|
||||
for _, c := range a.sess.World.ActiveChunks() {
|
||||
if time.Since(start) > 6*time.Millisecond {
|
||||
break // 帧预算(区块管理.md §4.3.2)
|
||||
}
|
||||
if !c.Dirty.Load() {
|
||||
continue
|
||||
}
|
||||
m := a.meshBuilder.Build(c, func(x, y, z int32) block.State {
|
||||
return a.sess.World.Block(x, y, z)
|
||||
dx := float64(c.CX)*16 + 8 - px
|
||||
dz := float64(c.CZ)*16 + 8 - pz
|
||||
dirty = append(dirty, dirtyJob{c: c, d: dx*dx + dz*dz})
|
||||
}
|
||||
sort.Slice(dirty, func(i, j int) bool { return dirty[i].d < dirty[j].d })
|
||||
for _, dj := range dirty {
|
||||
if time.Since(start) > 6*time.Millisecond {
|
||||
break // 帧预算(区块管理.md §4.3.2)
|
||||
}
|
||||
// 整个构建持一次读锁:邻居查询免逐次加锁(渲染.md §3.3)
|
||||
a.sess.World.WithReadLock(func() {
|
||||
m := a.meshBuilder.Build(dj.c, func(x, y, z int32) block.State {
|
||||
return a.sess.World.BlockUnlocked(x, y, z)
|
||||
})
|
||||
a.mu.Lock()
|
||||
a.pendingMesh = append(a.pendingMesh, &worldMeshJob{cx: dj.c.CX, cz: dj.c.CZ, m: m})
|
||||
a.mu.Unlock()
|
||||
})
|
||||
a.mu.Lock()
|
||||
a.pendingMesh = append(a.pendingMesh, &worldMeshJob{cx: c.CX, cz: c.CZ, m: m})
|
||||
a.mu.Unlock()
|
||||
c.Dirty.Store(false)
|
||||
dj.c.Dirty.Store(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +242,35 @@ func (a *clientApp) Render() {
|
||||
}
|
||||
a.uiR.Flush()
|
||||
a.rend.EndFrame()
|
||||
|
||||
// 截图(dev 测试:游戏内等玩家所在区块 5×5 网格就绪再拍,避免视野空洞导致误判)
|
||||
a.frames++
|
||||
if *shotPath != "" && a.frames >= 200 {
|
||||
ready := a.screen != 1 // 非游戏画面(标题/暂停)无需等区块
|
||||
if !ready {
|
||||
ready = true
|
||||
pcx, pcz := int32(a.sess.Player.Pos[0])>>4, int32(a.sess.Player.Pos[2])>>4
|
||||
for cx := pcx - 2; cx <= pcx+2 && ready; cx++ {
|
||||
for cz := pcz - 2; cz <= pcz+2; cz++ {
|
||||
if !a.rend.HasChunk(cx, cz) {
|
||||
ready = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ready || a.frames > 2400 { // 就绪即拍;48 秒兜底
|
||||
if !ready {
|
||||
a.logf("截图兜底:视野未完全就绪(frame=%d)", a.frames)
|
||||
}
|
||||
if err := a.saveScreenshot(*shotPath); err != nil {
|
||||
a.logf("截图失败: %v", err)
|
||||
} else {
|
||||
a.logf("截图已保存: %s", *shotPath)
|
||||
}
|
||||
a.rend.Window().SetShouldClose(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldClose 窗口关闭判定。
|
||||
@@ -320,9 +380,24 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 游戏会话(玩家先高空下落,区块加载后落地)
|
||||
p := physics.NewPlayer(8.5, 100, 8.5)
|
||||
cam := camera.New(p.Eye(), 0, 0, cfg.Client.FOV)
|
||||
// 4) 游戏会话(出生逻辑:先等出生区块加载,再定位地表出生——
|
||||
// 否则玩家从高空坠落会穿过未加载区块掉进地下,游戏内一片漆黑)
|
||||
var surf int32 = 120
|
||||
for deadline := time.Now().Add(15 * time.Second); time.Now().Before(deadline); {
|
||||
w.Update(8.5, 100, 8.5, 8*time.Millisecond)
|
||||
if w.Block(8, 80, 8) != block.Air {
|
||||
break
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
for y := int32(255); y > 0; y-- {
|
||||
if w.Block(8, y, 8) != block.Air {
|
||||
surf = y
|
||||
break
|
||||
}
|
||||
}
|
||||
p := physics.NewPlayer(8.5, float64(surf+2), 8.5)
|
||||
cam := camera.New(p.Eye(), 0, -0.35, cfg.Client.FOV) // 略微俯视,视野内以地形为主
|
||||
inv := inventory.New(items)
|
||||
sess := game.NewSession(w, reg, items, recipes, p, inv, cam)
|
||||
|
||||
|
||||
33
cmd/client/devshot_gl.go
Normal file
33
cmd/client/devshot_gl.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build gl
|
||||
|
||||
// 开发测试工具:自动进游戏 + 截图(dev 测试通过后再打包)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
)
|
||||
|
||||
// saveScreenshot 从默认帧缓冲读取像素并保存为 PNG(渲染线程调用)。
|
||||
func (a *clientApp) saveScreenshot(path string) error {
|
||||
w, h := int32(a.winW), int32(a.winH)
|
||||
pix := make([]uint8, w*h*4)
|
||||
gl.ReadPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(pix))
|
||||
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
|
||||
// 垂直翻转(GL 原点在左下)
|
||||
for y := int32(0); y < h; y++ {
|
||||
srcRow := (h - 1 - y) * w * 4
|
||||
dstRow := y * w * 4
|
||||
copy(img.Pix[dstRow:dstRow+w*4], pix[srcRow:srcRow+w*4])
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建截图文件: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, img)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// Build 打包纹理键列表为图集。
|
||||
// resolve:纹理键 → PNG 文件路径;缺失纹理跳过(键不进入 uv 表,运行时用占位纹理)。
|
||||
// 列数取「大于等于根号的整数」,使图集接近正方形(GPU 友好)。
|
||||
// 尺寸取 2 的次幂(mipmap 兼容:非 2 次幂图集在部分驱动上 GenerateMipmap 失败会致纹理不完整)。
|
||||
func Build(resolve func(key string) (string, bool), keys []string, tileSize int) (image.Image, map[string]image.Rectangle, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil, fmt.Errorf("atlas.Build: 纹理列表为空")
|
||||
@@ -27,6 +27,9 @@ func Build(resolve func(key string) (string, bool), keys []string, tileSize int)
|
||||
cols++
|
||||
}
|
||||
rows := (len(keys) + cols - 1) / cols
|
||||
// 补齐到 2 的次幂(mipmap 要求)
|
||||
cols = nextPow2(cols)
|
||||
rows = nextPow2(rows)
|
||||
img := image.NewRGBA(image.Rect(0, 0, cols*tileSize, rows*tileSize))
|
||||
uv := make(map[string]image.Rectangle, len(keys))
|
||||
|
||||
@@ -50,6 +53,15 @@ func Build(resolve func(key string) (string, bool), keys []string, tileSize int)
|
||||
return img, uv, nil
|
||||
}
|
||||
|
||||
// nextPow2 返回不小于 n 的最小 2 次幂。
|
||||
func nextPow2(n int) int {
|
||||
p := 1
|
||||
for p < n {
|
||||
p <<= 1
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// loadPNG 解码 PNG。
|
||||
func loadPNG(path string) (image.Image, error) {
|
||||
f, err := os.Open(path)
|
||||
|
||||
@@ -32,26 +32,27 @@ func (c *Camera) Right() [3]float64 {
|
||||
}
|
||||
|
||||
// View 视图矩阵(列主序 4×4,GL 用)。
|
||||
// 推导:相机旋转 R_cam = RotY(yaw)·RotX(pitch);视图 = R_camᵀ·T(-pos)。
|
||||
// 旧实现矩阵布局错误导致非零视角下画面全灰(白屏/灰屏根因之一)。
|
||||
func (c *Camera) View() [16]float32 {
|
||||
// 旋转 = RotX(-pitch) × RotY(-yaw),再平移 -pos
|
||||
cp, sp := math.Cos(-c.Pitch), math.Sin(-c.Pitch)
|
||||
cy, sy := math.Cos(-c.Yaw), math.Sin(-c.Yaw)
|
||||
cp, sp := math.Cos(c.Pitch), math.Sin(c.Pitch)
|
||||
cy, sy := math.Cos(c.Yaw), math.Sin(c.Yaw)
|
||||
|
||||
// R = RotY(-yaw) × RotX(-pitch) 的计算结果(行主序推导后转列主序)
|
||||
// 列主序布局: m[col*4+row]
|
||||
m := [16]float32{}
|
||||
var m [16]float32
|
||||
// 列 0
|
||||
m[0] = float32(cy)
|
||||
m[1] = float32(-sy * sp)
|
||||
m[2] = float32(-sy * cp)
|
||||
m[4] = float32(sy)
|
||||
m[5] = float32(cy * sp)
|
||||
m[6] = float32(cy * cp)
|
||||
m[8] = 0
|
||||
m[9] = float32(cp)
|
||||
m[10] = float32(-sp)
|
||||
m[1] = 0
|
||||
m[2] = float32(sy)
|
||||
// 列 1
|
||||
m[4] = float32(-sy * sp)
|
||||
m[5] = float32(cp)
|
||||
m[6] = float32(cy * sp)
|
||||
// 列 2
|
||||
m[8] = float32(-sy * cp)
|
||||
m[9] = float32(-sp)
|
||||
m[10] = float32(cy * cp)
|
||||
m[15] = 1
|
||||
|
||||
// 平移部分:t = -R·pos
|
||||
// 平移:t = -R·pos
|
||||
px, py, pz := float32(c.Pos[0]), float32(c.Pos[1]), float32(c.Pos[2])
|
||||
m[12] = -(m[0]*px + m[4]*py + m[8]*pz)
|
||||
m[13] = -(m[1]*px + m[5]*py + m[9]*pz)
|
||||
|
||||
@@ -13,6 +13,12 @@ import (
|
||||
// chunkKey 区块坐标 → key(与 world 包一致)。
|
||||
func chunkKey(cx, cz int32) uint64 { return uint64(uint32(cx))<<32 | uint64(uint32(cz)) }
|
||||
|
||||
// HasChunk 区块网格是否已上传(dev 截图测试等待视野就绪用)。
|
||||
func (r *Renderer) HasChunk(cx, cz int32) bool {
|
||||
_, ok := r.chunks[chunkKey(cx, cz)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// UploadChunk 上传区块网格到 GPU(dirty 网格重建后调用,渲染.md §3.3)。
|
||||
// 三类网格合并为单 VBO + 单 IBO:不透明 → cutout → 半透明 顺序连续存放。
|
||||
func (r *Renderer) UploadChunk(cx, cz int32, m *mesh.ChunkMesh) {
|
||||
|
||||
@@ -80,6 +80,11 @@ func New(title string, width, height int) (*Renderer, error) {
|
||||
gl.Enable(gl.DEPTH_TEST)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
// 背面剔除:区块边界面向未加载邻居的内侧面从外部看是背面,不剔除会以暗色贴图
|
||||
// 出现在视野里("脚下黑块"根因);同时省一半片元工作量。
|
||||
// 本渲染器方块网格按 CW 绕序定义正面(mesh.faceDef 角顺序)。
|
||||
gl.Enable(gl.CULL_FACE)
|
||||
gl.FrontFace(gl.CW)
|
||||
gl.ClearColor(0.53, 0.81, 0.92, 1) // 天空蓝
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -146,14 +146,15 @@ func textrWidth(at *font.Atlas, s string) int {
|
||||
}
|
||||
|
||||
// UploadImage 上传图像为纹理并返回 ID(物品图标/长文本用)。
|
||||
// 注意:UI 纹理按 1:1 像素风绘制,不使用 mipmap——
|
||||
// 非 2 次幂纹理 GenerateMipmap 在部分驱动上失败会导致纹理不完整、采样为纯白(白屏根因)。
|
||||
func (r *Renderer) UploadImage(img image.Image) uint32 {
|
||||
var tex uint32
|
||||
gl.GenTextures(1, &tex)
|
||||
gl.BindTexture(gl.TEXTURE_2D, tex)
|
||||
b := img.Bounds()
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(b.Dx()), int32(b.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(imageBytesUI(img)))
|
||||
gl.GenerateMipmap(gl.TEXTURE_2D)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
@@ -274,7 +275,8 @@ void main() {
|
||||
}
|
||||
`
|
||||
|
||||
// uiFragSrc UI 片元着色器(纹理 × 颜色,alpha 混合)。
|
||||
// uiFragSrc UI 片元着色器:纹理色 × 颜色 tint(alpha 混合)。
|
||||
// 注意:必须输出纹理 RGB——否则彩色纹理(按钮/背景)会被丢弃成纯白(白屏根因)。
|
||||
const uiFragSrc = `
|
||||
#version 330 core
|
||||
in vec2 vUV;
|
||||
@@ -283,7 +285,7 @@ uniform sampler2D uTex;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 t = texture(uTex, vUV);
|
||||
fragColor = vec4(vColor.rgb, vColor.a) * vec4(1.0, 1.0, 1.0, t.a);
|
||||
fragColor = t * vColor;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const (
|
||||
|
||||
// World 世界实例。
|
||||
type World struct {
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
chunks map[uint64]*chunk.Chunk
|
||||
states map[uint64]chunkState
|
||||
|
||||
@@ -170,9 +170,31 @@ func (w *World) Block(x, y, z int32) block.State {
|
||||
if y < 0 || y >= chunk.Height {
|
||||
return block.Air
|
||||
}
|
||||
w.mu.Lock()
|
||||
w.mu.RLock()
|
||||
ch := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))]
|
||||
w.mu.RUnlock()
|
||||
if ch == nil {
|
||||
return block.Air
|
||||
}
|
||||
return ch.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))
|
||||
}
|
||||
|
||||
// WithReadLock 在持有世界读锁期间执行 fn。
|
||||
// 网格构建用它包住整个 Build:邻居查询走 BlockUnlocked,免去每格一次加锁
|
||||
// (旧实现每个面邻居查询各加一次锁,单区块约 40 万次加锁,mesh 构建极慢,
|
||||
// 出生区块迟迟无网格,玩家透过脚下的洞看到相邻区块地下横截面——"进游戏黑屏"主因之一)。
|
||||
func (w *World) WithReadLock(fn func()) {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
fn()
|
||||
}
|
||||
|
||||
// BlockUnlocked 无锁读方块(仅在 WithReadLock 回调内调用;未加载区块与世界边界视为空气)。
|
||||
func (w *World) BlockUnlocked(x, y, z int32) block.State {
|
||||
if y < 0 || y >= chunk.Height {
|
||||
return block.Air
|
||||
}
|
||||
ch := w.chunks[coordKey(coord.ChunkCoord(x), coord.ChunkCoord(z))]
|
||||
w.mu.Unlock()
|
||||
if ch == nil {
|
||||
return block.Air
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
|
||||
Name "${PRODUCT_NAME}"
|
||||
OutFile "..\dist\${INSTALLER_NAME}"
|
||||
InstallDir "$PROGRAMFILES64\${PRODUCT_NAME_EN}"
|
||||
InstallDirRegKey HKLM "${UNINSTALL_KEY}" "InstallLocation"
|
||||
RequestExecutionLevel admin
|
||||
; 按用户安装(游戏需要往自身目录写日志/存档,Program Files 无写入权限会导致启动崩溃)
|
||||
InstallDir "$LOCALAPPDATA\Programs\${PRODUCT_NAME_EN}"
|
||||
InstallDirRegKey HKCU "${UNINSTALL_KEY}" "InstallLocation"
|
||||
RequestExecutionLevel user
|
||||
SetCompressor /SOLID lzma
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
@@ -68,15 +69,15 @@ Section "主程序" SEC01
|
||||
FileWrite $0 "世界存档目录:游戏会自动把世界保存到这里。"
|
||||
FileClose $0
|
||||
|
||||
; 注册卸载信息
|
||||
WriteRegStr HKLM "${UNINSTALL_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr HKLM "${UNINSTALL_KEY}" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "${UNINSTALL_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKLM "${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXE}"
|
||||
WriteRegStr HKLM "${UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegStr HKLM "${UNINSTALL_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegDWORD HKLM "${UNINSTALL_KEY}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "${UNINSTALL_KEY}" "NoRepair" 1
|
||||
; 注册卸载信息(按用户安装 → HKCU)
|
||||
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKCU "${UNINSTALL_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXE}"
|
||||
WriteRegStr HKCU "${UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegStr HKCU "${UNINSTALL_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegDWORD HKCU "${UNINSTALL_KEY}" "NoModify" 1
|
||||
WriteRegDWORD HKCU "${UNINSTALL_KEY}" "NoRepair" 1
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
@@ -98,5 +99,5 @@ Section "Uninstall"
|
||||
RMDir /r "$INSTDIR\resourcepacks"
|
||||
RMDir /r "$INSTDIR\mods"
|
||||
; 保留用户存档(worlds/)与日志,不删除
|
||||
DeleteRegKey HKLM "${UNINSTALL_KEY}"
|
||||
DeleteRegKey HKCU "${UNINSTALL_KEY}"
|
||||
SectionEnd
|
||||
|
||||
Reference in New Issue
Block a user