Skip to content

Conversation

@JiepengTan
Copy link
Contributor

@JiepengTan JiepengTan commented Dec 28, 2025

test project: 99-01Platform.zip

related pr: goplus/godot#232

how to export tilemap data : goplus/godot#232

onKey KeyA, => {
	LoadTilemap("tilemaps/map1")
}
onKey KeyB, => {
	LoadTilemap("tilemaps/map2")
}
MutilTilemap.mp4

@JiepengTan JiepengTan changed the title Support full tilemap export and import in SPX v2. 【WIP】Support full tilemap export and import in SPX v2. Dec 28, 2025
@gemini-code-assist
Copy link

Summary of Changes

Hello @JiepengTan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the tilemap system in SPX v2 by enabling full export and import functionality, primarily through the integration of a new C++-based tilemap parser. This change allows for a more advanced and structured tilemap format, separating decorator data into its own JSON file for better organization. The update touches various layers of the codebase, from core tilemap management logic to code generation and FFI bindings, ensuring seamless interaction between Go and the underlying C++ engine for enhanced game development capabilities.

Highlights

  • New Tilemap Format Support: Introduced support for a new tilemap JSON format (version >= 1) which utilizes a C++ TileMapParser for loading and processing tilemap data, enhancing capabilities for complex tilemap structures.
  • Separate Decorator Loading: Decorators for tilemaps are now loaded from a separate decorator.json file when using the new tilemap format, allowing for more modular and flexible scene composition.
  • New Tilemapparser Manager: A new ITilemapparserMgr interface and its corresponding implementation (tilemapparserMgr) have been added to manage the loading, unloading, and querying of tilemaps, including functions like LoadTilemap, UnloadTilemap, DestroyAllTilemaps, HasTilemap, and GetTilemapLayerCount.
  • Code Generation Improvements: Refined the code generation logic for GDExtension bindings to accurately identify and register manager functions, ensuring correct FFI integration for new and existing managers.
  • Type Renaming for Clarity: Renamed internal types vec2 to Vec2 and decoratorNode to DecoratorNode within the internal/tilemap package for improved consistency and readability.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for a new tilemap format, which involves adding a TilemapparserMgr and updating various generated files and export configurations. The core logic changes are in tilemap.go to handle the new format and in the codegen scripts to correctly resolve manager names.

My review focuses on the non-generated code. I've identified an opportunity to refactor duplicated code in the codegen scripts to improve maintainability and fix a potential panic. I also found a recurring typo in a struct definition within internal/tilemap/tilemap.go that should be corrected for clarity and correctness. Overall, the changes look good and move towards a more robust tilemap handling system.

Comment on lines +98 to +130
// getManagerNameForFunc extracts the manager name from the function name.
func getManagerNameForFunc(funcName string, knownManagers []string) string {
prefix := "GDExtensionSpx"
str := funcName[len(prefix):]
lowerStr := strings.ToLower(str)

// Prefer matching against known manager names (sorted by length descending, prioritizing longer names)
if len(knownManagers) > 0 {
// Create a copy sorted by length in descending order
sortedNames := make([]string, len(knownManagers))
copy(sortedNames, knownManagers)
sort.Slice(sortedNames, func(i, j int) bool {
return len(sortedNames[i]) > len(sortedNames[j])
})

for _, mgr := range sortedNames {
if strings.HasPrefix(lowerStr, mgr) {
return mgr
}
}
}

// Fall back to the original logic (stop at uppercase letter)
chs := []rune{}
chs = append(chs, rune(str[0]), rune(str[1]))
for _, ch := range str[2:] {
if ch >= 'A' && ch <= 'Z' {
break
}
chs = append(chs, ch)
}
return strings.ToLower(string(chs))
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function getManagerNameForFunc duplicates logic from GetManagerName in pkg/gdspx/cmd/codegen/generate/common/funcs.go. This can lead to maintenance issues. For example, this implementation uses an ASCII-only check for uppercase letters (ch >= 'A' && ch <= 'Z'), while the other uses the more robust unicode.IsUpper.

Additionally, the fallback logic in both functions is unsafe and will panic if the input string str has fewer than 2 characters, as it accesses str[0] and str[1] without a length check.

I recommend consolidating this logic into a single, robust function. Since common already imports clang, you could export this function from the clang package (e.g., as GetManagerNameForFunc) and reuse it in the common package.

Here is a suggested improved implementation that is safe and uses Unicode-aware checks. Please note you will need to add import "unicode" to use it.

func getManagerNameForFunc(funcName string, knownManagers []string) string {
	prefix := "GDExtensionSpx"
	str := funcName[len(prefix):]
	lowerStr := strings.ToLower(str)

	// Prefer matching against known manager names (sorted by length descending, prioritizing longer names)
	if len(knownManagers) > 0 {
		// Create a copy sorted by length in descending order
		sortedNames := make([]string, len(knownManagers))
		copy(sortedNames, knownManagers)
		sort.Slice(sortedNames, func(i, j int) bool {
			return len(sortedNames[i]) > len(sortedNames[j])
		})

		for _, mgr := range sortedNames {
			if strings.HasPrefix(lowerStr, mgr) {
				return mgr
			}
		}
	}

	// Fall back to the original logic (stop at uppercase letter)
	if len(str) < 2 {
		return strings.ToLower(str)
	}
	for i, ch := range str[2:] {
		if unicode.IsUpper(ch) {
			return strings.ToLower(str[:i+2])
		}
	}
	return strings.ToLower(str)
}

Scale vec2 `json:"scale,omitempty"`
Position Vec2 `json:"position"`
Scale Vec2 `json:"scale,omitempty"`
Ratation float64 `json:"rotation,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the field name Ratation. It should be Rotation. This also affects the JSON tag. Correcting this would improve code clarity and correctness. A similar typo exists in the spriteNode struct on line 108.

Suggested change
Ratation float64 `json:"rotation,omitempty"`
Rotation float64 `json:"rotation,omitempty"`

Scale vec2 `json:"scale,omitempty"`
Position Vec2 `json:"position"`
Scale Vec2 `json:"scale,omitempty"`
Ratation float64 `json:"rotation,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the field name Ratation. It should be Rotation. This also affects the JSON tag.

Suggested change
Ratation float64 `json:"rotation,omitempty"`
Rotation float64 `json:"rotation,omitempty"`

Scale vec2 `json:"scale,omitempty"`
Position Vec2 `json:"position"`
Scale Vec2 `json:"scale,omitempty"`
Ratation float64 `json:"rotation,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Typo in field name

Ratation should be Rotation. While the JSON tag is correct (json:"rotation"), the Go field name is misspelled, which affects code readability and the public API since DecoratorNode is exported.

This typo also appears in:

  • spriteNode struct (line 108)
  • Usage in tilemap.go:125

Recommendation: Rename to Rotation throughout the codebase.

tilemap.go Outdated

// isNewFormat checks if the tilemap JSON is in the new format (version >= 1)
// New format uses C++ TileMapParser with Base64 encoded tile_map_data
func (p *gameTilemapMgr) isNewFormat(fs spxfs.Dir, path string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Redundant JSON parsing

The isNewFormat method performs a full JSON parse just to read the version field. This is immediately followed by another complete parse (line 72 for old format, or C++ loader for new format).

For large tilemap files (>500KB), this doubles the parsing overhead.

Recommendations:

  1. Parse once and inspect the version field from the parsed data
  2. Or use a streaming JSON parser to peek at the version field
  3. Cache the parsed result to avoid re-reading

position := item.Position.ToVec2()
pivot := item.Pivot.ToVec2()
assetPath := engine.ToAssetPath("tilemaps/" + item.Path)
relativePath := path.Join(tilemapDir, item.Path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Potential path traversal

User-controlled item.Path from decorator.json is joined with tilemapDir without validation. An attacker could use path traversal sequences like "../../sensitive/file.png" to access assets outside the intended directory.

While filepath.Clean() is eventually called in ToAssetPath(), it happens after path concatenation.

Recommendation:

// Validate decorator path
cleanPath := filepath.Clean(item.Path)
if strings.Contains(cleanPath, "..") {
    fmt.Printf("Warning: Skipping decorator with invalid path: %s\n", item.Path)
    continue
}
relativePath := path.Join(tilemapDir, item.Path)

var data DecoratorJSON
err := loadJson(&data, fs, decoratorPath)
if err != nil {
fmt.Printf("[TILEMAP] No decorator.json found at %s (this is OK if no decorators)\n", decoratorPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Error handling could be more specific

The message "No decorator.json found" appears for any error, not just missing files. JSON parsing errors are silently treated as "OK".

Recommendation:

if err != nil {
    if os.IsNotExist(err) {
        // OK - decorator.json is optional
        return
    }
    fmt.Printf("[TILEMAP] Warning: Failed to parse decorator.json: %v\n", err)
    return
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

原来的AI-Town工程需要重新用插件导出,兼容吗?

tilemap.go Outdated
}

// isNewFormat checks if the tilemap JSON is in the new format (version >= 1)
// New format uses C++ TileMapParser with Base64 encoded tile_map_data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation: Potentially misleading comment

The comment mentions "Base64 encoded tile_map_data" but there's no evidence in the Go code of Base64 decoding. The C++ TileMapParser receives a file path, not decoded data.

Please clarify if Base64 encoding is handled internally by the C++ parser, or remove the reference if inaccurate.

@xgopilot
Copy link
Contributor

xgopilot bot commented Dec 28, 2025

Code Review Summary

This PR successfully adds dual-format tilemap support with good backward compatibility. However, several critical issues need attention before merge.

Critical Issues

  1. Typo: "Ratation" → "Rotation" in exported types (impacts public API)
  2. Bug: ConvertData function ineffective - loop modifies copies instead of actual elements
  3. Performance: Double JSON parsing in version detection adds unnecessary overhead

Security Concerns

  • Path traversal risk in decorator loading - validate paths before using
  • Missing validation for ColliderParams arrays from JSON

Code Quality

  • Duplicated tile parsing logic between files
  • Error handling treats all failures as "file not found"
  • Missing documentation for exported types

The architecture is sound and the dual-loader approach is well-structured. Please address the critical issues, particularly the typo and the ConvertData bug, as these affect functionality and the public API.

@JiepengTan JiepengTan marked this pull request as draft December 28, 2025 13:49
@JiepengTan JiepengTan force-pushed the pr_tilemap_full_support branch from 33bda90 to 47580b0 Compare January 4, 2026 08:58
@JiepengTan JiepengTan changed the title 【WIP】Support full tilemap export and import in SPX v2. Support full tilemap export and import in SPX v2. Jan 4, 2026
@JiepengTan JiepengTan marked this pull request as ready for review January 4, 2026 09:00
position := item.Position.ToVec2()
pivot := item.Pivot.ToVec2()
assetPath := engine.ToAssetPath("tilemaps/" + item.Path)
relativePath := path.Join(tilemapDir, item.Path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Path Traversal Vulnerability

The item.Path from decorator JSON is joined without validation, allowing potential path traversal attacks. An attacker could craft a malicious JSON with paths like "../../../etc/passwd".

Suggested change
relativePath := path.Join(tilemapDir, item.Path)
// Validate path to prevent traversal attacks
if !filepath.IsLocal(item.Path) {
panic(fmt.Sprintf("Invalid decorator path: %s", item.Path))
}
relativePath := path.Join(tilemapDir, item.Path)

Reference: CWE-22

tilemap.go Outdated
// Old format: use existing Go loader
var data tm.TscnMapData
err := loadJson(&data, fs, path)
err := loadJson(&data, fs, tilemapPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Redundant JSON Parsing

The tilemap JSON is parsed twice - once in isNewFormat() to check the version, then again here. For large tilemap files, this doubles I/O and parsing overhead (30-50% slowdown).

Consider parsing once and checking the version field in memory instead.

tilemap.go Outdated
// New format: use C++ TileMapParser for loading tilemap
enginePath := engine.ToAssetPath(tilemapPath)
fmt.Printf("[TILEMAP] Using C++ TileMapParser for: %s\n", enginePath)
tilemapparserMgr.LoadTilemap(enginePath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Inconsistent Error Handling

The old format panics on load failure (good), but the new format silently continues with no error checking from LoadTilemap(). This makes debugging new format failures difficult.

Consider adding error checking or at minimum logging the operation's success/failure.

Scale vec2 `json:"scale,omitempty"`
Position Vec2 `json:"position"`
Scale Vec2 `json:"scale,omitempty"`
Ratation float64 `json:"rotation,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Typo in Field Name

The field is named Ratation but should be Rotation. The JSON tag is correct, but the Go field name is misspelled. This appears in both DecoratorNode and spriteNode.

If this format is already in production, add a comment acknowledging the typo. Otherwise, fix it before release.

@xgopilot
Copy link
Contributor

xgopilot bot commented Jan 4, 2026

Code Review Summary

The tilemap export/import implementation is architecturally sound with clear separation between old and new formats. However, several critical issues need attention:

Security (High Priority):

  • Path traversal vulnerability in decorator loading - validate paths with filepath.IsLocal()
  • Integer overflow risk in world size calculations - add bounds checking
  • Unbounded memory allocation from tile data - impose reasonable limits

Performance:

  • Double JSON parsing wastes 30-50% initialization time - parse once and check version in memory

Code Quality:

  • Field typo: RatationRotation in structs
  • Inconsistent error handling between old/new formats
  • Code duplication in tile parsing logic

See inline comments for details and suggested fixes.

tilemap.go Outdated
// New format uses C++ TileMapParser with Base64 encoded tile_map_data
func (p *gameTilemapMgr) isNewFormat(fs spxfs.Dir, path string) bool {
var versionCheck struct {
Version int `json:"version"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation: Missing Explanation

The asymmetric adjustments (minY - 1 vs maxX + 1) need better explanation. Why subtract 1 for minY but not minX? This suggests a coordinate system assumption that should be documented in the function comment.

func (p *gameTilemapMgr) parseTilemap() {
// Handle new format: load decorators from separate JSON file
if p.useNewLoader {
p.loadDecoratorsFromJSON()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Missing World Size Calculation

World size is only calculated for old format (calcWorldSize() at line 174). Is this intentional for new format? If the C++ parser handles it, add a comment explaining why it's skipped.

tilemap.go Outdated
var versionCheck struct {
Version int `json:"version"`
}
if err := loadJson(&versionCheck, fs, path); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Error Handling Silently Fails

When isNewFormat() encounters a JSON parsing error, it returns false, treating corrupted files as old format. This masks real issues and makes debugging harder.

Consider distinguishing between "file doesn't exist", "malformed JSON", and "old format without version field".

)

// DecoratorJSON represents the structure of decorator.json file (new format)
type DecoratorJSON struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation: Version Field Semantics Missing

The Version field lacks documentation explaining:

  • Valid version numbers (currently >= 1 is new format)
  • What version 0 or missing means (old format)
  • Future version compatibility
Suggested change
type DecoratorJSON struct {
// DecoratorJSON represents the structure of decorator.json file (new format)
type DecoratorJSON struct {
// Version indicates decorator JSON format version.
// Version >= 1: new format with separate decorator.json
// Version 0 or missing: old format (combined with tilemap in TscnMapData)
Version int `json:"version"`
Decorators []tm.DecoratorNode `json:"decorators"`
}

@JiepengTan JiepengTan force-pushed the pr_tilemap_full_support branch from 28f4421 to 1667962 Compare January 4, 2026 13:49
@JiepengTan JiepengTan force-pushed the pr_tilemap_full_support branch from 1667962 to 7d1df08 Compare January 7, 2026 12:48
@joeykchen joeykchen merged commit 3eec957 into goplus:dev Jan 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants