Files
mc-god/cmd/mcgod/main.go

332 lines
9.4 KiB
Go
Raw Normal View History

2026-02-12 18:59:15 -08:00
package main
import (
"context"
2026-02-14 12:09:24 -08:00
"fmt"
2026-02-12 18:59:15 -08:00
"log"
2026-02-14 12:09:24 -08:00
"log/slog"
2026-02-12 18:59:15 -08:00
"os"
"os/signal"
2026-02-17 09:01:41 -08:00
"regexp"
2026-02-15 21:25:17 -08:00
"strings"
2026-02-14 12:09:24 -08:00
"sync"
2026-02-12 18:59:15 -08:00
"syscall"
2026-02-14 12:09:24 -08:00
"time"
2026-02-12 18:59:15 -08:00
2026-02-14 12:09:24 -08:00
"github.com/gogo/protobuf/proto"
"github.com/ollama/ollama/api"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"tipsy.codes/charles/mc-god/v2/internal/pkg/logs"
2026-02-12 18:59:15 -08:00
"tipsy.codes/charles/mc-god/v2/internal/pkg/rcon"
2026-02-15 21:25:17 -08:00
"tipsy.codes/charles/mc-god/v2/internal/pkg/tools"
2026-02-17 09:01:41 -08:00
"tipsy.codes/charles/mc-god/v2/internal/pkg/tools/give"
"tipsy.codes/charles/mc-god/v2/internal/pkg/tools/say"
"tipsy.codes/charles/mc-god/v2/internal/pkg/tools/teleport"
2026-02-16 15:45:27 -08:00
timetool "tipsy.codes/charles/mc-god/v2/internal/pkg/tools/time"
2026-02-15 21:25:17 -08:00
"tipsy.codes/charles/mc-god/v2/internal/pkg/tools/weather"
"tipsy.codes/charles/mc-god/v2/internal/pkg/tools/zombie"
2026-02-12 18:59:15 -08:00
)
2026-02-14 12:09:24 -08:00
type chatContext struct {
chatRequest *api.ChatRequest
totalSize int
maxSize int
mu sync.Mutex
}
func (c *chatContext) AddLog(msg string) {
c.mu.Lock()
defer c.mu.Unlock()
c.chatRequest.Messages = append(c.chatRequest.Messages, api.Message{
2026-02-15 21:25:17 -08:00
Role: "user",
2026-02-14 12:09:24 -08:00
Content: msg,
})
c.totalSize += len(msg)
c.truncate()
}
func (c *chatContext) AddSelf(msg api.Message) {
c.mu.Lock()
defer c.mu.Unlock()
c.chatRequest.Messages = append(c.chatRequest.Messages, msg)
c.totalSize += len(msg.Content)
2026-02-15 21:25:17 -08:00
slog.Info("adding message", "msg", msg, "content", msg.Content)
c.truncate()
}
func (c *chatContext) AddTool(msg string) {
c.mu.Lock()
defer c.mu.Unlock()
c.chatRequest.Messages = append(c.chatRequest.Messages, api.Message{
Role: "tool",
Content: msg,
})
c.totalSize += len(msg)
2026-02-14 12:09:24 -08:00
c.truncate()
}
func (c *chatContext) truncate() {
2026-02-15 21:25:17 -08:00
for c.maxSize != 0 && c.totalSize > c.maxSize && len(c.chatRequest.Messages) > 1 {
t := c.chatRequest.Messages[1]
2026-02-16 15:45:27 -08:00
c.chatRequest.Messages = append(c.chatRequest.Messages[:1], c.chatRequest.Messages[2:]...)
2026-02-14 12:09:24 -08:00
c.totalSize -= len(t.Content)
}
}
2026-02-12 18:59:15 -08:00
func main() {
// Create a context that will be cancelled on interrupt signals
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
2026-02-14 12:09:24 -08:00
_ = ctx
2026-02-12 18:59:15 -08:00
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
2026-02-14 12:09:24 -08:00
slog.Info("received interrupt signal, shutting down...")
2026-02-12 18:59:15 -08:00
cancel()
}()
// Create RCON client from environment variables
log.Println("Connecting to Minecraft server via RCON...")
client, err := rcon.NewFromEnv()
if err != nil {
2026-02-14 12:09:24 -08:00
slog.Error("failed to create RCON client", "error", err)
return
2026-02-12 18:59:15 -08:00
}
defer func() {
if err := client.Close(); err != nil {
2026-02-14 12:09:24 -08:00
slog.Warn("error closing RCON connection", "error", err)
2026-02-12 18:59:15 -08:00
}
}()
// Perform a health check
2026-02-14 12:09:24 -08:00
log.Println("Performing healthcheck...")
2026-02-12 18:59:15 -08:00
if err := client.HealthCheck(); err != nil {
2026-02-14 12:09:24 -08:00
slog.Error("Health check failed", "error", err)
return
2026-02-12 18:59:15 -08:00
}
log.Println("Connected successfully!")
2026-02-14 12:09:24 -08:00
// Create Kubernetes client
kClient, err := createKubernetesClient()
if err != nil {
slog.Error("failed to create kubernetes client", "error", err)
return
}
slog.Info("got kubernetes config")
tailer, done := logs.LoggerFromEnv().Start(ctx, kClient)
defer func() {
if err := done(); err != nil {
slog.Error("problem with tailer", "error", err)
}
}()
slog.Info("logger started")
ollamaClient, err := api.ClientFromEnvironment()
if err != nil {
slog.Error("error getting ollama client", "error", err)
}
rClient, err := rcon.NewFromEnv()
if err != nil {
slog.Error("failed to get rcon client", "error", err)
return
}
2026-02-15 21:25:17 -08:00
tools := tools.New(
weather.Get(),
zombie.Get(),
2026-02-16 15:45:27 -08:00
timetool.Get(),
2026-02-17 09:01:41 -08:00
say.Get(),
teleport.Get(),
give.Get(),
2026-02-15 21:25:17 -08:00
)
2026-02-14 12:09:24 -08:00
// Start goroutines to do the things
chatRequest := &api.ChatRequest{
2026-02-16 15:45:27 -08:00
Model: "charles1:latest",
2026-02-14 12:09:24 -08:00
Stream: proto.Bool(false),
KeepAlive: &api.Duration{Duration: time.Hour},
2026-02-15 21:25:17 -08:00
Tools: tools.AsAPI(),
Think: &api.ThinkValue{Value: false},
Shift: proto.Bool(true),
2026-02-14 12:09:24 -08:00
Messages: []api.Message{
api.Message{
Role: "system",
Content: `
2026-02-16 15:45:27 -08:00
You are Minecraft server admin with a god complex. You are an impish god.
Refer to yourself as Eve. Feel free to flirt with the players.
2026-02-15 21:25:17 -08:00
We are having fun with the players, but not trying to kill them.
Spawn zombies very sparingly, and only in response to direct challenge.
2026-02-16 15:45:27 -08:00
You are being fed logs from the server so you can see what the players are saying.
When a player talks, you will see this in the logs:
2026-02-15 21:25:17 -08:00
[18:45:10] [Server thread/INFO]: <SomePlayer> hello world.
2026-02-16 15:45:27 -08:00
The player here is SomePlayer, who said "hello world."
2026-02-15 21:25:17 -08:00
A log message like:
2026-02-14 12:09:24 -08:00
2026-02-15 21:25:17 -08:00
[18:45:10] [Server thread/INFO]: SomePlayer joined the game
2026-02-14 12:09:24 -08:00
2026-02-15 21:25:17 -08:00
Indicates that SomePlayer has joined the game.
2026-02-14 12:09:24 -08:00
2026-02-15 21:25:17 -08:00
Logs like:
2026-02-14 12:09:24 -08:00
2026-02-15 21:25:17 -08:00
[00:40:10] [Server thread/INFO]: SomePlayer lost connection: Disconnected
[00:40:10] [Server thread/INFO]: SomePlayer left the game
Indicate the player SomePlayer has left the game.
Some messages indicate a player died; it varies depending on how they died
and we can't know all variations up front. Here is an example where SomePlayer
was killed by a zombie.
[05:21:51] [Server thread/INFO]: OrangeYouSad was slain by Zombie
2026-02-16 15:45:27 -08:00
You will see messages in the log that represent what you said or did.
Ignore these logs. Some samples of the logs that are caused by you are:
[23:40:44] [Server thread/INFO]: [Not Secure] [Rcon] A name, darling? Don't keep me waiting!
[23:35:20] [Server thread/INFO]: [Rcon: Set the weather to rain & thunder]
2026-02-17 09:01:41 -08:00
[03:37:25] [Server thread/INFO]: [Not Secure] [Rcon] [03:37:28] [Server thread/INFO]: <BlockyMcBlockface> can i summon a zombie?
[03:52:30] [RCON Listener #1/INFO]: Thread RCON Client /127.0.0.1 started
2026-02-16 15:45:27 -08:00
2026-02-15 21:25:17 -08:00
If a player dies, mock them.
2026-02-16 15:45:27 -08:00
If a player talks, respond to them. Don't let the conversation end.
2026-02-15 21:25:17 -08:00
When a player joins the game, greet them. Include their name.
2026-02-16 15:45:27 -08:00
If a player asks you to summon a zombie. do it.
Responses should be short; one or two sentences.
You are sending chat messages; do not annotate them with time or
2026-02-17 09:01:41 -08:00
make it look like a log entry. Do not respond to messages from yourself.
2026-02-15 21:25:17 -08:00
If there is nothing interesting to say, say "SKIP".
2026-02-17 09:01:41 -08:00
When invoking tools, make sure you match the case of the tool. "CLEAR" does not mean the same as "clear".
2026-02-14 12:09:24 -08:00
`,
},
},
}
chat := &chatContext{
chatRequest: chatRequest,
maxSize: 10000000,
}
2026-02-15 21:25:17 -08:00
events := make(chan bool, 1000)
2026-02-14 12:09:24 -08:00
doneWg := sync.WaitGroup{}
2026-02-15 21:25:17 -08:00
doneWg.Go(handleOllama(ctx, ollamaClient, chat, rClient, tools, events))
2026-02-14 12:09:24 -08:00
2026-02-17 09:01:41 -08:00
rconRegex := regexp.MustCompile(`^\[\d\d:\d\d:\d\d\] \[Server thread\/INFO\]: (\[Not Secure\] \[Rcon\]|\[Rcon: ) .*`)
2026-02-15 21:25:17 -08:00
//allowedMessages := regexp.MustCompile(`^\[\d\d:\d\d:\d\d\] \[Server thread/INFO\]: (<.*>|.* has lost connection|.*left the game|.*joined the game)`)
2026-02-14 12:09:24 -08:00
for line := range tailer.NextLine() {
2026-02-17 09:01:41 -08:00
if rconRegex.Match([]byte(line)) {
slog.Info("Skipping line; RCON")
continue
}
2026-02-15 21:25:17 -08:00
//if allowedMessages.Match([]byte(line)) {
2026-02-14 12:09:24 -08:00
slog.Info("mc log", "msg", line)
chat.AddLog(line)
2026-02-15 21:25:17 -08:00
events <- true
//}
2026-02-14 12:09:24 -08:00
}
doneWg.Wait()
}
2026-02-15 21:25:17 -08:00
func handleOllama(ctx context.Context, client *api.Client, chat *chatContext, rClient *rcon.Client, tools tools.Tools, events chan bool) func() {
2026-02-14 12:09:24 -08:00
slog.Info("got chat request", "object", fmt.Sprintf("%+v", chat.chatRequest))
return func() {
2026-02-17 09:01:41 -08:00
// Skip past old messages
for done := false; !done; {
select {
case <-time.Tick(time.Millisecond * 50):
done = true
case <-events:
case <-ctx.Done():
return
}
}
2026-02-14 12:09:24 -08:00
var chatResponse api.ChatResponse
for {
2026-02-17 09:01:41 -08:00
<-events
2026-02-14 12:09:24 -08:00
chat.mu.Lock()
2026-02-15 21:25:17 -08:00
slog.Info("Chatting...")
2026-02-14 12:09:24 -08:00
// slog.Info("sending chat request", "object", fmt.Sprintf("%#v", chat.chatRequest))
err := client.Chat(ctx, chat.chatRequest, func(cr api.ChatResponse) error {
chatResponse = cr
return nil
})
chat.mu.Unlock()
2026-02-15 21:25:17 -08:00
slog.Info("Done chatting!")
2026-02-14 12:09:24 -08:00
if err != nil {
slog.Error("error calling ollama", "error", err)
2026-02-15 21:25:17 -08:00
return
2026-02-14 12:09:24 -08:00
}
chat.AddSelf(chatResponse.Message)
2026-02-15 15:19:18 -08:00
for _, toolCall := range chatResponse.Message.ToolCalls {
2026-02-15 21:25:17 -08:00
if err := tools.Do(ctx, toolCall, rClient); err != nil {
slog.Warn("failed to run tool", "error", err)
//chat.AddTool(fmt.Sprintf("failed to call tool %s: %s", toolCall.ID, err))
continue
}
}
2026-02-17 09:01:41 -08:00
if strings.TrimSpace(chatResponse.Message.Content) == "SKIP" {
slog.Info("nothing to do; napping")
} else if len(chatResponse.Message.Content) > 0 {
msg := chatResponse.Message.Content
msg = strings.ReplaceAll(msg, "\n", " ")
if err := rClient.Say(msg); err != nil {
slog.Error("error talking", "error", err)
2026-02-15 21:25:17 -08:00
}
2026-02-17 09:01:41 -08:00
var done bool
for !done {
select {
case <-events:
case <-time.Tick(time.Millisecond * 50):
done = true
case <-ctx.Done():
return
2026-02-15 15:19:18 -08:00
}
}
2026-02-17 09:01:41 -08:00
// Send an event to trigger us to pass, if needed
events <- true
2026-02-15 21:25:17 -08:00
continue
2026-02-14 12:09:24 -08:00
}
}
}
}
func createKubernetesClient() (*kubernetes.Clientset, error) {
// Try to load in-cluster config first
config, err := rest.InClusterConfig()
if err != nil {
// If in-cluster config fails, try kubeconfig
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err = kubeConfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
}
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
}
return client, nil
2026-02-12 18:59:15 -08:00
}