I got the bridge working and the duel technically running, but my confident intern (Claude) led me down a rabbit hole of trying to parse the byte-based messages from scratch. There was real progress for a bit, but after reaching a broken state where messages were being parsed incorrectly and data sizes were misaligned, I took a step back and decided to change how the communication would work.
ygopro-core directly references YGOpen for message protocol definitions. Instead of continuing to reverse-engineer byte layouts manually, I’ll be implementing that encoder/decoder layer properly and using protobuf definitions as the contract between systems.
Before you can create a duel, the engine requires three function pointers wired into OCG_DuelOptions:
cardReader — called whenever the engine needs a card's stats (ATK, DEF, type, race, etc.) given a card codescriptReader — called whenever the engine needs to load a Lua script for a card effectlogHandler — called to emit log messagesThese are implemented as C functions inside the CGo preamble (the comment block above import "C"), which is the standard pattern for CGo callbacks. For now they’re stubs:
cardReader returns a zeroed-out card structscriptReader returns 0 (not found)logHandler does nothingThat’s enough to satisfy the engine's requirements and get a duel handle.
/*
#cgo LDFLAGS: -L${SRCDIR} -locgcore
#include "ocgapi.h"
#include <stdlib.h>
#include <string.h>
// cardReader: called by engine to get card stats by code.
// For now we return a blank card so the engine doesn't crash.
void cardReaderStub(void* payload, uint32_t code, OCG_CardData* data) {
memset(data, 0, sizeof(OCG_CardData));
data->code = code;
}
// cardReaderDone: called after cardReader so we can free memory.
// Nothing to free in our stub.
void cardReaderDoneStub(void* payload, OCG_CardData* data) {}
// scriptReader: called by engine to load a Lua script by name.
// Returning 0 means "script not found" — engine will skip it.
int scriptReaderStub(void* payload, OCG_Duel duel, const char* name) {
return 0;
}
// logHandler: called by engine to emit log messages.
void logHandlerStub(void* payload, const char* str, int type) {
// We'll wire this to Go's logger later
}
*/
With these wired in, OCG_CreateDuel returned a valid duel handle — a pointer to a live game state object in C++ memory.
To exercise the engine, we added 5 copies of Dark Magician (card code 46986414) to player 0's hand, plus 40-card decks for both players, then called OCG_StartDuel.
// Add 5 copies of Dark Magician (code 46986414) to player 0's hand
fori:=0;i<5;i++ {
bridge.AddCard(duel,46986414,0,bridge.LOC_HAND,uint32(i),bridge.POS_FACEUP)
}
// Add 40 copies to player 0's deck so the engine doesn't immediately error
fori:=0;i<40;i++ {
bridge.AddCard(duel,46986414,0,bridge.LOC_DECK,uint32(i),bridge.POS_FACEDOWN)
}
// Same for player 1
fori:=0;i<40;i++ {
bridge.AddCard(duel,46986414,1,bridge.LOC_DECK,uint32(i),bridge.POS_FACEDOWN)
}
bridge.StartDuel(duel)
fmt.Println("Duel started, processing...")